Use AWS Copilot CLI to deploy containers on existing infrastructure - Tutorial
Tags: aws, devops, docker, cloud
Published: 2022-02-13 11:27:50

---
title: Use AWS Copilot CLI to deploy containers on existing infrastructure - Tutorial
published: true
date: 2021-04-21 05:50:03 UTC
tags: aws, devops, docker, cloud
canonical_url: https://towardsthecloud.com/aws-copilot-cli-deploy-containers-tutorial
---
## Introduction

When you start a project where you're containerizing your applications, it's pretty easy to get started locally and develop your container images. But when you plan to deploy these containers in the Cloud it can be quite a challenge and a lot of work to set up the required infrastructure to run these containers.

That's where [AWS Copilot CLI](https://aws.github.io/copilot-cli/) comes in. This is a tool that allows developers to build, release, and operate production-ready containerized applications on Amazon ECS and AWS Fargate. Essentially it does the heavy lifting for you and creates the required infrastructure using CloudFormation. That means in a matter of minutes, you're able to deploy your containers in your AWS account. Where normally it would take at least a day to set up the required orchestration using CloudFormation.

One of the most useful features of AWS Copilot that I discovered [(released in v0.3)](https://aws.amazon.com/about-aws/whats-new/2020/08/aws-copilot-cli-launches-v0-3-operations-configurations/) was that you can configure AWS Copilot to use pre-existing VPC, subnets, and CIDR ranges. This is a huge advantage when you're in the middle of the development phase of a project.

You might ask, why is that the case? That's because you can use your existing Infra as Code tool to deploy resources such as RDS, Elasticache, and EFS and connect them to your AWS Copilot containers. That way you can quickly set up a working container environment. In the meantime, you or your teammates can work on replicating what you've deployed with AWS Copilot in your preferred Infra as Code tool e.g. CloudFormation, CDK, or Terraform.

So in this post, I'll show you how you can deploy containers **on your existing VPC with AWS Copilot**. As a reference case, we'll be deploying the following Django example App and combine it with cloud-native services like Elasticache (Redis), RDS (Postgress) & Secrets Manager.

%[https://github.com/dannysteenman/docker-django-example] 


**Table of Contents**

- [Introduction](#introduction)
- [Prerequisites](#prerequisites)
- [1. Initializing the App](#1-initializing-the-app)
- [2. Initialize the test environment in the existing VPC](#2-initialize-the-test-environment-in-the-existing-vpc)
- [3. Deploy RDS + Elasticache](#3-deploy-rds--elasticache)
- [4. Initialize the Django app service](#4-initialize-the-django-app-service)
- [5. Update the manifest with the RDS and Redis connection variables](#5-update-the-manifest-with-the-rds-and-redis-connection-variables)
- [6. Deploy the Django example service](#6-deploy-the-django-example-service)
- [7. Login on the container](#7-login-on-the-container)
- [8. Wrapping up: Delete the service](#8-wrapping-up-delete-the-service)

## Prerequisites

The following **prerequisites** are needed for the container deployment with this AWS Copilot tutorial:
- [Install Docker](https://docs.docker.com/get-docker/)
- [Install AWS Copilot CLI](https://aws.github.io/copilot-cli/docs/getting-started/install/)
- Set up an [AWS Account](https://docs.aws.amazon.com/polly/latest/dg/setting-up.html#setting-up-signup)
- Deploy your preferred VPC (including subnets) or use the template I created for this [tutorial](https://github.com/dannysteenman/docker-django-example/blob/main/cloudformation/vpc.yaml)

Once the prerequisites are set up, we can start with the next steps to use AWS Copilot to deploy the service within the existing VPC.

## 1. Initializing the App

First clone the project that we're going to use for this example:

```bash
❯ git clone git@github.com:dannysteenman/docker-django-example.git
```

Then we're going to initialize the application in AWS Copilot:

>
An application is a group of related services, environments, and pipelines. Whether you have one service that does everything or a constellation of micro-services, Copilot organizes them and the environments they're deployed to into an "application".
[Source](https://aws.github.io/copilot-cli/docs/concepts/applications/)

In this case, we deploy one AWS Copilot App with the following command:

```bash
❯ copilot app init django-example-copilot
```

![terminal command: copilot app init django-example-copilot](https://cdn.hashnode.com/res/hashnode/image/upload/v1618312083943/zxviSoh7g.png)

It will create a `copilot/.workspace` file in the root of your project folder and deploys the required roles on the account which you exported in your CLI.

## 2. Initialize the test environment in the existing VPC

Now we're going to set up the [environment](https://aws.github.io/copilot-cli/docs/concepts/environments/) in which we want to deploy the application. Since we're going to use our existing VPC we're going to add the `--import-vpc-id` argument. By doing this we prevent AWS Copilot from creating it's own VPC and networking:

```bash
❯ copilot env init -n test --import-vpc-id vpc-0a0a8fe59ebbe825a --import-public-subnets subnet-0bf192b5dd6b69353,subnet-0e84a756ecfe675d8,subnet-0e85448003c9363e6 --import-private-subnets subnet-02b1d1c235d8b7b0a,subnet-021979e65464bffa5,subnet-0968fa1ad198daed9
```

This deploys the ECS cluster in the imported VPC and uses 3 public subnets and 3 private subnets (separated over 3 AZ's).

![terminal command to deploy copilot in an existing vpc](https://cdn.hashnode.com/res/hashnode/image/upload/v1618313435002/eGh0iTqXH.png)

## 3. Deploy RDS + Elasticache

For this setup, we're going to connect the containers with an RDS database and Elasticache Redis node. These are services that don't require containerized setup and require less maintenance than containers.

In the repository, you'll find [2 CloudFormation templates](https://github.com/dannysteenman/docker-django-example/tree/main/cloudformation) that deploy a Postgres RDS with AWS Secretsmanager and an Elasticache Redis cluster. Deploy these templates in your existing VPC and make sure to fill the parameters with the correct values.

## 4. Initialize the Django app service

The repository contains the docker file that builds the Django image. We're going to create a [Load Balanced Web Service](https://aws.github.io/copilot-cli/docs/manifest/lb-web-service/). This will create an ALB with a target group that allows us to access the Django container from the internet. To do that we need to run AWS Copilot to set up the manifest file to make it ready for deployment.

```bash
❯ copilot svc init --name django-example --svc-type "Load Balanced Web Service" --dockerfile ./Dockerfile
```

This will create the ECR repository in which AWS Copilot will upload the container image for that specific service.

![terminal command to deploy an aws copilot load balanced service](https://cdn.hashnode.com/res/hashnode/image/upload/v1618324432419/KYAD16kaP.png)

It will also generate a manifest file in `copilot/django-example/manifest.yml`. This [file](https://github.com/dannysteenman/docker-django-example/blob/main/copilot/django-example/manifest.yml) is used by AWS Copilot to generate and deploy a CloudFormation template for that specific ECS Service. The manifest gives you an easy way to adjust the configuration of your container and service, the generated file looks like this:

```YAML
# The manifest for the "django-example" service.
# Read the full specification for the "Load Balanced Web Service" type at:
#  https://aws.github.io/copilot-cli/docs/manifest/lb-web-service/

# Your service name will be used in naming your resources like log groups, ECS services, etc.
name: django-example
type: Load Balanced Web Service

# Distribute traffic to your service.
http:
  # Requests to this path will be forwarded to your service.
  # To match all requests you can use the "/" path.
  path: "/"
  # You can specify a custom health check path. The default is "/".
  # healthcheck: '/'

# Configuration for your containers and service.
image:
  # Docker build arguments. For additional overrides: https://aws.github.io/copilot-cli/docs/manifest/lb-web-service/#image-build
  build: Dockerfile
  # Port exposed through your container to route traffic to it.
  port: 8000

cpu: 256 # Number of CPU units for the task.
memory: 512 # Amount of memory in MiB used by the task.
count: 1 # Number of tasks that should be running in your service.
exec: true # Enable running commands in your container.

# Optional fields for more advanced use-cases.
#
#variables:                    # Pass environment variables as key-value pairs.
#  LOG_LEVEL: info

#secrets:                      # Pass secrets from AWS Systems Manager (SSM) Parameter Store.
#  GITHUB_TOKEN: GITHUB_TOKEN  # The key is the name of the environment variable, the value is the name of the SSM parameter.

# You can override any of the values defined above by environment.
#environments:
#  test:
#    count: 2               # Number of tasks to run for the "test" environment.

```

## 5. Update the manifest with the RDS and Redis connection variables

The original docker-compose file of this project creates separate containers for the database and cache. But since we replaced them with their Cloud-native counterparts (RDS and Elasticache), we need to inject variables in the container, so they can be used by the Django application settings.

The Django app settings (`src/config/settings.py`) luckily already support environment variables for the Database and Redis settings. So all you need to do is add your own custom variables into the manifest:

```YAML
variables:
  ALLOWED_HOSTS: "*"
  DEBUG: true
  NODE_ENV: development
  POSTGRES_DB: postgres
  POSTGRES_HOST: <insert the endpoint from the RDS resource you created>
  POSTGRES_PORT: 5432
  REDIS_URL: redis://<insert the Redis endpoint from the Elasticache resource you created>:6379/0
  SECRET_KEY: change_me


secrets:
  POSTGRES_USER: "'arn:aws:secretsmanager:region:accountid:secret:secretname:username::'"
  POSTGRES_PASSWORD: "'arn:aws:secretsmanager:region:accountid:secret:secretname:password::'"
```

A colleague of mine [found out](https://github.com/aws/copilot-cli/issues/2132) by trial and error that it's possible to use AWS Secrets Manager in the AWS Copilot manifest. So we're using that to securely pass the secret into the container.

**Note:** Make sure to tag your AWS Secrets or AWS Parameters with the following tags (key-value):

```
copilot-environment: test
copilot-application: django-example
```

This is necessary in order for AWS Copilot to make use of the parameters and secrets. The execution role in the CloudFormation template that AWS Copilot deploys contains a condition to only accept tagged secrets and parameters that are intended to be used for this tool. Down below you see the actual template that AWS Copilot generates:

```YAML
  ExecutionRole:
    Metadata:
      'aws:copilot:description': 'An IAM Role for the Fargate agent to make AWS API calls on your behalf'
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: 'sts:AssumeRole'
      Policies:
        - PolicyName: !Join ['', [!Ref AppName, '-', !Ref EnvName, '-', !Ref WorkloadName, SecretsPolicy]]
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: 'Allow'
                Action:
                  - 'ssm:GetParameters'
                Resource:
                  - !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/*'
                Condition:
                  StringEquals:
                    'ssm:ResourceTag/copilot-application': !Sub '${AppName}'
                    'ssm:ResourceTag/copilot-environment': !Sub '${EnvName}'
              - Effect: 'Allow'
                Action:
                  - 'secretsmanager:GetSecretValue'
                Resource:
                  - !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:*'
                Condition:
                  StringEquals:
                    'secretsmanager:ResourceTag/copilot-application': !Sub '${AppName}'
                    'secretsmanager:ResourceTag/copilot-environment': !Sub '${EnvName}'
```

## 6. Deploy the Django example service

Now that we've updated the manifest it will look something like this:

```bash
# The manifest for the "django-example" service.
# Read the full specification for the "Load Balanced Web Service" type at:
#  https://aws.github.io/copilot-cli/docs/manifest/lb-web-service/

# Your service name will be used in naming your resources like log groups, ECS services, etc.
name: django-example
type: Load Balanced Web Service

# Distribute traffic to your service.
http:
  # Requests to this path will be forwarded to your service.
  # To match all requests you can use the "/" path.
  path: "/"
  # You can specify a custom health check path. The default is "/".
  # healthcheck: '/'

# Configuration for your containers and service.
image:
  # Docker build arguments. For additional overrides: https://aws.github.io/copilot-cli/docs/manifest/lb-web-service/#image-build
  build: Dockerfile
  # Port exposed through your container to route traffic to it.
  port: 8000

cpu: 256 # Number of CPU units for the task.
memory: 512 # Amount of memory in MiB used by the task.
count: 1 # Number of tasks that should be running in your service.
exec: true # Enable running commands in your container.

# Optional fields for more advanced use-cases.
#
variables: # Pass environment variables as key value pairs.
  ALLOWED_HOSTS: "*"
  DEBUG: true
  NODE_ENV: development
  POSTGRES_DB: postgres
  POSTGRES_HOST: <insert the endpoint from the RDS resource you created>
  POSTGRES_PORT: 5432
  REDIS_URL: redis://<insert the Redis endpoint from the Elasticache resource you created>:6379/0
  SECRET_KEY: change_me

secrets: # Update the region, accountid and secretname of the Secret Manager secret
  POSTGRES_USER: "'arn:aws:secretsmanager:region:accountid:secret:secretname:username::'"
  POSTGRES_PASSWORD: "'arn:aws:secretsmanager:region:accountid:secret:secretname:password::'"

# You can override any of the values defined above by environment.
#environments:
#  test:
#    count: 2               # Number of tasks to run for the "test" environment.
```

Don't forget to update the Redis and Postgress endpoints, which you deployed earlier. We can then use this manifest to deploy the container into our ECS cluster. Run the following command to deploy the service with AWS Copilot:

```bash
❯ copilot svc deploy --name django-example --env test
```

It will now start building the Docker image from the Dockerfile in your repository:

![terminal command: copilot svc deploy --name django-example --env test](https://cdn.hashnode.com/res/hashnode/image/upload/v1618329034664/7sVKRHHDN.png)

After creating the image it will push it to ECR and start deploying it by updating the CloudFormation template. From the CLI you're able to watch the progression of the deployment and eventually it should finish successfully.

![show how aws copilot finishes deploying a service](https://cdn.hashnode.com/res/hashnode/image/upload/v1618329132180/ZH83qoDEs.png)

Once the deployment is finished it will provide the URL of the Application Loadbalancer and if you click it, you'll see the Django example app!

![the result will display the example django app](https://cdn.hashnode.com/res/hashnode/image/upload/v1618329467352/ZQ09pR4PW.png)

## 7. Login on the container

With `copilot svc exec` you're able to execute a command on the running container. This makes it possible to start an interactive bash session on the Django app by:

```bash
❯ copilot svc exec --name django-example --env test -c /bin/bash
```

![terminal command: copilot svc exec --name django-example --env test -c /bin/bash](https://cdn.hashnode.com/res/hashnode/image/upload/v1618341523899/F8-QZBGUs.png)

This is just as intuitive as running `docker exec` but then for containers hosted on AWS. The advantage of having this ability is that it will tremendously speed up your container development because you can log in on the container where you can test and troubleshoot issues instantly. Once you find anything in the container that needs optimization, you can then update the source file of the app in the repository and repeat [step 6](#6-deploy-the-django-example-service) to deploy your changes in the Cloud again.

Optionally you can also display the logs of the container directly by running:

```bash
❯ copilot svc logs --name django-example --env test
```

![terminal command: copilot svc logs --name django-example --env test](https://cdn.hashnode.com/res/hashnode/image/upload/v1618341530840/Us_rprAwZ.png)

## 8. Wrapping up: Delete the service

If you're done with testing the container and want to clean up your AWS Copilot resources, we can delete the service by:

```bash
❯ copilot svc delete
```

Or better, if you want to delete everything we've created to run AWS Copilot, then we can:

```bash
❯ copilot app delete --yes
```

![terminal command: copilot app delete --yes](https://cdn.hashnode.com/res/hashnode/image/upload/v1618342899381/AHcDBC9OM.png)

---
👋 **Enjoyed this article?** Reach out in the comments below or on [Twitter](https://twitter.com/dannysteenman) to let me know what you think of it.