Containers

Streamline Windows Container Deployment on Amazon ECS with AWS Copilot and AWS Fargate

Since AWS Copilot CLI launched in 2020, developers have been using the tool to build, manage, and operate Linux containers successfully on Amazon Elastic Container Service (Amazon ECS) and AWS Fargate. By leaving the infrastructure-knitting and resource-wrangling to AWS Copilot, builders can spend more time focused on their business logic. With yesterday’s launch of Amazon ECS on AWS Fargate, Windows developers can now take advantage of AWS Copilot’s intuitive, guided experience to launch their apps in minutes.

AWS Copilot follows Amazon ECS best practices by default (read about our design tenets), and offers plenty of flexibility for customization. To launch a Windows container, start with a Windows-based Dockerfile on your Windows machine (no need for WSL!). Under the hood, AWS Copilot will detect your platform and build your container image for Windows. Then, Copilot will deploy your app on Amazon ECS with Fargate for Windows. That’s it— you’re in business!

Getting started

Let’s take a closer look at each step by deploying an example app together. We’re going to build and launch a load-balanced Flask app that talks to an Amazon DynamoDB table.

  1. Installation: Make sure you have the Windows Copilot binary downloaded, AWS CLI configured with a default profile, and Docker running.
  2. Dockerfile and source code: The Dockerfile contains step-by-step instructions for building your image, including those to copy your source code into the container.
Directory structure:
├─example-app
│ ├─Dockerfile
│ ├─requirements.txt
| ├─server.py
│ ├─templates
    └─jokes.html

Dockerfile

FROM python:3.7-windowsservercore-1809

WORKDIR /user/src/app

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt --user
COPY server.py .
COPY templates/jokes.html ./templates/jokes.html

ENTRYPOINT ["python", "server.py"]
EXPOSE 5000

Our Dockerfile is building on a Windows base image, python:3.7-windowsservercore-1809. Base images must be for Windows Server 2019. We create a working directory, then copy and install dependencies from our source requirements.txt file.

requirements.txt

Flask==2.0.1
boto3==1.18.43

We then copy the entry point file for our Flask app and the Jinja2 template containing basic HTML.

server.py

from flask import Flask, render_template
import boto3
from boto3.dynamodb.conditions import Key
import random
import os

app = Flask(__name__)

visitors = 0
ddb_resource = boto3.resource('dynamodb', region_name='ap-southeast-1')
table_name = os.environ.get('JOKES_NAME')
table = ddb_resource.Table(table_name)

@app.route("/mem")
def mem():
    global visitors
    visitors += 1
    return f"Hello, visitor #{visitors}! Get ready to laugh!"

@app.route("/ddb")
def ddb():    
    populate_table()
    joke = tell_joke()
    return render_template('jokes.html', question=joke['Question'], answer=joke['Answer'])

def populate_table():
    with table.batch_writer() as batch:
        batch.put_item(Item={"Number": 1, "Question": "What's brown and sticky?", "Answer": "A stick!"})
        batch.put_item(Item={"Number": 2, "Question": "What's orange and sounds like a parrot?", "Answer": "A carrot!"})
        batch.put_item(Item={"Number": 3, "Question": "Why was six afraid of seven?", "Answer": "Because seven ate nine!"})
        batch.put_item(Item={"Number": 4, "Question": "What's a cucumber's favorite instrument?", "Answer": "A pickle-o!"})
        batch.put_item(Item={"Number": 5, "Question": "Why don't seagulls fly over the bay?", "Answer": "Because then they'd be bagels!"})
        batch.put_item(Item={"Number": 6, "Question": "What did the baker say after they sold out of pita bread?", "Answer": "We have naan!"})
        batch.put_item(Item={"Number": 7, "Question": "What did the robber take from the music store?", "Answer": "The lute!"})
        batch.put_item(Item={"Number": 8, "Question": "If April showers bring May flowers, what do Mayflowers bring?", "Answer": "Pilgrims!"})
        batch.put_item(Item={"Number": 9, "Question": "How much soda do tropical birds drink?", "Answer": "Toucans!"})
        batch.put_item(Item={"Number": 10, "Question": "How many tickles does it take to tickle an octopus?", "Answer": "TENtacles!"})
        batch.put_item(Item={"Number": 11, "Question": "What do you call a factory that makes only acceptable?", "Answer": "A satisFACTORY!"})
    return "done populating table"

def tell_joke():
    jokeNumber = random.randint(1, 11)
    table = ddb_resource.Table(table_name)
    try: 
        resp = table.get_item(Key={"Number": jokeNumber})
    except Exception as e:
        print(e.response['Error']['Message'])
    else:
        return resp['Item']

@app.route("/")
def home():
    return "Visit /ddb for some belly laughs or /mem to be reminded of '90s-era web counters."

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

templates/jokes.html

<!doctype html>
<title>Joker!</title>

  <h1>  Q: {{ question }} </h1>
  <h1>  A: {{ answer }} </h1>
  1. Init: From your source directory, you can run the copilot init command to start the magic and launch your app. The command copilot init is like a combo meal that includes copilot app init, copilot svc init, copilot env init, and copilot svc deploy, with some friendly prompts along the way (“Would you like some fries with that to deploy a test environment?”) Watch the screen recording to see copilot init in action!

For today’s walkthrough, let’s run each copilot init subcommand separately to examine what exactly is happening with each step, and explore opportunities for customization.

Name and create your application with copilot app init

GIF of copilot app init

What you’re doing:
With copilot app init, you are naming and creating your application, the overarching grouping of related workloads (services, scheduled jobs, one-off tasks) and environments.

What AWS Copilot is doing:
At this point, AWS Copilot creates the IAM roles needed to manage the release infrastructure, as well as a copilot\ subdirectory in your working directory. Any AWS Copilot-generated manifests or additional infrastructure will live here.

Options:
You may pass in flags with copilot app init : --domain to specify an existing domain name that you’d like to use, and --resource-tags to add custom tags to resources provisioned for this app.

Name and create your application’s environment with copilot env init

GIF of copiliot env init

What you’re doing:
Here, you are creating and naming an environment, which you might think of as a stage of deployment. Many people like to have separate ‘test’ and ‘prod’ environments. Other use environments as a sandboxes for developers or teams of developers. In this step, you are prompted for environment credentials specific to the environment you are initializing. You can create your environments with AWS accounts and in Regions that are different than those of your app!

What AWS Copilot is doing:
Under the hood, AWS Copilot generates all of the AWS resources required for networking (VPC, subnets, security groups, route tables, internet gateway, IAM roles). It also creates the resources to be shared among the workloads deployed in this environment (Amazon ECS cluster, Application Load Balancer).

Options:
If you already have a VPC and would rather not spin up another one, or if you have specific networking requirements (for instance, you want only private subnets), you may import your own existing resources at the prompt.

copilot env init has flags that enable you to pass in the various required values if you’d rather skip the prompts. It also has a few flags to indicate CIDRs for subnets/VPC if you don’t want to use the defaults.

Run copilot svc init

GIF of copilot svc init

What you’re doing:
When you run copilot svc init, AWS Copilot prompts you to choose a service type. AWS Copilot sets up your infrastructure in the copilot svc deploy step based on what kind of service (Load Balanced Web Service, Backend service, or Worker service) you are building. Review our docs for more details. After you name your service and confirm the path to your Dockerfile, you have the option to update your manifest file (more on that to come).

What AWS Copilot is doing:
At copilot svc init, AWS Copilot sets up an Amazon Elastic Container Registry (Amazon ECR) repository and registers your service to AWS Systems Manager Parameter Store. The CLI also takes a peek at your Docker engine to see what platform (OS/Architecture) you’re on, and looks for any port and health check details. Because it is not the default platform (linux/amd64), AWS Copilot surfaces your Windows platform in the manifest generated in this step. The CLI also adjusts values automatically in your manifest (adjusting CPU and memory values, disabling exec and efs) to accommodate Windows. The manifest file is stored in the copilot\ directory, created in the copilot app init step, as copilot\<your service\job name>\manifest.yml. This is where you can do a huge amount of configuration depending on your specific needs!

The following is the manifest that was generated for our example app (copilot\joker\manifest.yml):

Example app manifest for copilot\joker\manifest.yml

Options:
As you can see, there are many fun bells and whistles to play with in the manifest. There are even more possibilities than those that appear by default in the manifest: AWS Auto Scaling, target group health checks, source IP allow lists, and environment-specific overrides, to name a few. Review the documentation specific to your service type to learn more. Windows users can see the previously mentioned platform field in their manifests – this is where you can change your OSFamily. To do so, change the platform value from the generated string, which defaults to WINDOWS_SERVER_2019_CORE, to WINDOWS_SERVER_2019_FULL.

This string format:

platform: windows/x86_64

is equivalent to this map format:

platform:
  osfamily: windows_server_2019_core
  architecture: x86_64

You’ve come to expect the option to pass in required fields with flags, and copilot svc init is no different. Besides the flags for skipping prompts, there is also one for specifying the location of an existing Docker image. AWS Copilot won’t look for your Dockerfile to build and push a new image to Amazon ECR if you’d rather use an existing one.

AWS Copilot offers two additional ways to run containers: scheduled jobs, which are tasks that are triggered by an event, and one-off tasks. As with service workloads, the CLI will provision the right amount of AWS resources required for your purposes.

Add persistence to your application with copilot storage init

GIF copilot storage init

What you’re doing:
This optional step, which is not included in copilot init, enables you to add persistence (DynamoDB table, Amazon Aurora Serverless cluster, or Amazon S3 bucket) to your app. After you make naming and organizational decisions, take the generated environment variable and use it within your code, so that your container can reference the storage resource. Note that while AWS Copilot has Amazon Elastic File System (Amazon EFS) capabilities, Amazon EFS is not Windows-compatible.

What AWS Copilot is doing:
When you run the copilot storage init command, AWS Copilot creates an addons subdirectory in your copilot directory. You’ll see a new yaml file with the name you chose for your storage resource (Jokes.yml in our example). This AWS CloudFormation template will create the database or bucket you have designated, and will grant your service access to your resource.

Here is the template that was generated for our database (copilot\joker\addons\Jokes.yml):

Parameters:
  App:
    Type: String
    Description: Your application's name.
  Env:
    Type: String
    Description: The environment name your service, job, or workflow is being deployed to.
  Name:
    Type: String
    Description: The name of the service, job, or workflow being deployed.
Resources:
  Jokes:
    Metadata:
      'aws:copilot:description': 'An Amazon DynamoDB table for Jokes'
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Sub ${App}-${Env}-${Name}-Jokes
      AttributeDefinitions:
        - AttributeName: Number
          AttributeType: "N"
      BillingMode: PAY_PER_REQUEST
      KeySchema:
        - AttributeName: Number
          KeyType: HASH

  JokesAccessPolicy:
    Metadata:
      'aws:copilot:description': 'An IAM ManagedPolicy for your service to access the Jokes db'
    Type: AWS::IAM::ManagedPolicy
    Properties:
      Description: !Sub
        - Grants CRUD access to the Dynamo DB table ${Table}
        - { Table: !Ref Jokes }
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Sid: DDBActions
            Effect: Allow
            Action:
              - dynamodb:BatchGet*
              - dynamodb:DescribeStream
              - dynamodb:DescribeTable
              - dynamodb:Get*
              - dynamodb:Query
              - dynamodb:Scan
              - dynamodb:BatchWrite*
              - dynamodb:Create*
              - dynamodb:Delete*
              - dynamodb:Update*
              - dynamodb:PutItem
            Resource: !Sub ${ Jokes.Arn}
          - Sid: DDBLSIActions
            Action:
              - dynamodb:Query
              - dynamodb:Scan
            Effect: Allow
            Resource: !Sub ${ Jokes.Arn}/index/*

Outputs:
  JokesName:
    Description: "The name of this DynamoDB."
    Value: !Ref Jokes
  JokesAccessPolicy:
    Description: "The IAM::ManagedPolicy to attach to the task role."
    Value: !Ref JokesAccessPolicy

Options:
The copilot storage init command has some flags for passing in required fields to skip prompting, and others to skip prompting for optional fields altogether.

Deploy your application with copilot svc deploy:

What you’re doing:
Sitting back and watching the show.

What AWS Copilot is doing:
Because the manifest indicates that you’re building a Windows container, AWS Copilot passes that information on to Docker when running docker build. The CLI then pushes the built image to Amazon ECR.

Based on the type of service you chose to build in the copilot svc init step, and the configuration in your manifest, AWS Copilot provisions the appropriate resources to get you up and running, without extraneous expense.

In our example, we’re building an internet-facing Load Balanced Web Service. At deploy time, AWS Copilot spins up an Application Load Balancer to distribute incoming traffic, security groups to control traffic, and more. It does this on top of an Amazon ECS service, the heart of the whole operation. If you selected Worker Service, for example, you would get an Amazon ECS service with Amazon SQS for pub/sub capabilities. To see exactly what resources the CLI has generated for your service, run copilot svc show --resources. Similarly, to see what resources were spun up for your environment, use the copilot env show --resources command.

Not only does AWS Copilot tailor resources to the type of service, the CLI also considers the interplay of services within an app. For instance, all services come with Service Discovery, so a Load Balanced Web (frontend) Service can find and communicate with a Backend Service via private endpoints. The Backend Service itself doesn’t require any internet-facing endpoints. And remember all the specs in your manifest? AWS Copilot looks at the functionality you’ve requested and determines which resources you need to make those specifications happen. If you’ve indicated that you’d like tasks launched in private subnets, AWS Copilot will add NAT Gateways to an AWS Copilot-generated VPC. If you’ve added a secret, Amazon ECS Agent will work with AWS Systems Manager Parameter Store to pass the secrets as environment variables to your service securely.

Among the environment variables injected into your container, is the name of the storage resource you specified if you ran the previous command, copilot storage init. For our joker app, JOKES_NAME is the environment variable for our DynamoDB table. When you run copilot svc deploy, AWS Copilot creates your database or S3 bucket, creates a nested CloudFormation stack with the addons template (previous example), and grants your service access to the storage resource via an IAM Managed Policy.

copilot svc deploy is also the step at which AWS Copilot populates the Amazon ECS task definition’s OSFamily and PlatformVersion fields with Windows-specific values. For developers already familiar with Amazon ECS and AWS Copilot, the transition to launching Windows containers is seamless, as the workflow is identical–all platform-related business happens behind the scenes.

Options:
One useful flag available with copilot svc deploy is --force, which forces subsequent deployments even when there are no detected CloudFormation changes.

Our app is deployed! copilot svc deploy returned a URL at which we can view our Load-Balanced Web Service. You have the option to use an existing domain when you run app init, or use an alias for your service. Missed your URL? Access it, and more, with copilot svc show.

Deployed app screen

Cleaning up

Run copilot app delete:

What you’re doing:
The only thing easier than directing AWS Copilot to spin up your app is telling AWS Copilot to take it all down.

What AWS Copilot is doing:
This command deletes your app and all resources associated with it. (Note: your copilot subdirectory and its contents remain after you run copilot app delete.)

Options:
For the less commitment-phobic among us, there is a --yes flag that skips the “Are you sure?” confirmation prompt.

Conclusion

The AWS Copilot CLI guides you in building and deploying your projects, from simple, one-off tasks to multi-microservice apps with databases, sidecar containers, and more! AWS Copilot is an open-source tool and we encourage you to get involved by creating GitHub issues or joining the conversation on Gitter. See you there!