Getting Started with AWS CodeDeploy

Published:3 November 2021 - 17 min. read

Manual application deployments to an AWS EC2 virtual machine contribute to toil. Toil is the result of repetitive manual operations when you manage services in production. So how to avoid toil? AWS CodeDeploy is the key.

Not a reader? Watch this related video tutorial!
Not seeing the video? Make sure your ad blocker is disabled.

In this tutorial, you will learn about deploying applications to an Amazon EC2 instance with the AWS CodeDeploy service and eliminating unnecessary toil.

Read on to learn more!

Prerequisites

This tutorial will be a hands-on demonstration. If you’d like to follow along, be sure you have the following:

  • An AWS account. If you don’t have an AWS account, a free tier account is available.
  • An installed and configured AWS CLI.
  • Docker Desktop installed, this tutorial uses Docker 20.10.18 on a supported Linux or Windows operating system.

Understanding CodeDeploy

CodeDeploy is a deployment service that automates software deployments to AWS compute services. These AWS compute services are Elastic Compute Cloud (EC2), Fargate, Lambda functions, Elastic Compute Service, and even on-premise servers.

The deployment process of an application deployed via CodeDeploy is primarily configured through an AppSpec configuration file in either YAML or JSON format. AppSec specifies the type of AWS resource to operate on and is placed within the application’s folder to deploy.

Creating IAM Roles and Policies via the AWS Console

Before deploying an application with AWS CodeDeploy you must create an Identity and Access Management (IAM) role and policies to be used by CodeDeploy. An IAM role on AWS is a set of policies that grant users access to your AWS resources whereas an IAM policy contains the specific set of permissions to grant to a resource.

In this section, you will create a custom IAM Policy that allows EC2 to connect to AWS S3 and the Systems Manager services. Next, you will create two IAM roles with the appropriate trust policies via the AWS console to attach the necessary policies and allow access to CodeDeploy.

Creating the Custom IAM Policy to Manage an EC2 Instance

First, you must create a custom policy that grants access to AWS S3 and Systems Manager services that will be attached to an EC2 instance running the application, python-shortner-application.

You may also create the policy via the AWS CLI, but in this tutorial, the GUI method is shown.

1. Locate and open the IAM Policies section within your AWS management console by entering “IAM Policies” in the search box and clicking on Policies under Features.

Opening the IAM Policies section
Opening the IAM Policies section

2. From the Policies page, click the Create Policy button positioned at the top-right corner of the page to begin the three-step process of creating a new policy.

Creating a new IAM policy
Creating a new IAM policy

3. Within the Create Policy page, click the JSON tab to switch to the JSON editor and paste the JSON object below into the editor. The policy object below allows for data retrieval from the S3 service on AWS via the s3:Get* and s3:List* actions.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "s3:Get*",
                "s3:List*"
            ],
            "Effect": "Allow",
            "Resource": "*"
        }
    ]
}

Click on the Next: Tags button to proceed to the tags screen (no tags are necessary to assign) and then click the Next: Review button to proceed to the final step where you would review and name the new policy.

Adding policy object to JSON editor in Create Policy page
Adding policy object to JSON editor in Create Policy page

4. In the Review Policy step, specify a name, and description in the Policy Name and Description input fields, then click the Create Policy button to save the policy.

  • Policy name: CodeDeploy-EC2-Permission
  • Policy description: Policy to be attached to a role for an EC2 instance using CodeDeploy.
Naming the CodeDeploy-EC2-Permission policy
Naming the CodeDeploy-EC2-Permission policy

Creating the CodeDeploy-EC2-Permission IAM Role

Now that you have a newly created policy necessary for the correct EC2 permissions, you must create and then attach the policy to the role CodeDeploy-EC2-Permission that will be created below.

1. Locate and open the IAM Roles service section within your AWS management console by entering “IAM Roles” in the search box and clicking on Roles under Features.

Opening the IAM Roles section
Opening the IAM Roles section

2. Navigate back to the Roles page within the IAM console, and click the Create Role button at the top-right corner of the page to begin the process of creating a new role.

Clicking Create Role button to launch steps for creating an IAM role.
Clicking Create Role button to launch steps for creating an IAM role

3. On the Create Role page, select the EC2 option as a common use case and click the Next: Permissions button at the bottom of the page. Selecting EC2 as a common use case allows the EC2 instance to call other AWS services on your behalf.

Selecting EC2 as a common use case
Selecting EC2 as a common use case

4. At the Attach permissions policies page, enter CodeDeploy-EC2-Permission in the policies search bar and select the checkbox to add the CodeDeploy-EC2-Permission policy. Selecting the CodeDeploy-EC2-Permission policy gives the role access to the permissions object previously defined.

Attaching the CodeDeploy-EC2-Permission policy to the IAM role
Attaching the CodeDeploy-EC2-Permission policy to the IAM role

5. Next, enter SSM in the permissions search bar and click the checkbox next to the AmazonSSMManagedInstanceCore policy. The AmazonSSMManagedInstanceCore policy provides the IAM role access to the core functionality of the AWS Systems Manager, used to assist in managing EC2 instances.

Attaching AmazonSSMManagedInstanceCore policy to IAM role
Attaching AmazonSSMManagedInstanceCore policy to IAM role

Click the Next: Tags button to proceed to the Tags step (no tags are necessary to assign), and then click the Next: Review button to proceed to the last step where you would review and name the new role.

6. At the Review page, specify a Role name and Role description for this new role as defined below and click the Create Role button to save the role.

  • Role name: python-app-ec2-codedeploy-role
  • Role description: Role attached to an EC2 instance to use CodeDeploy.
Reviewing details of python-app-ec2-codedeploy-role
Reviewing details of python-app-ec2-codedeploy-role

Creating the IAM Role for CodeDeploy

After you have created the necessary role to allow an EC2 instance to access AWS S3 and SSM, you must create the IAM Role needed to allow CodeDeploy access.

1. From the same Roles page as used previously, click the Create Role button positioned at the top-right corner of the page to begin the four-step process of creating a new role.

Clicking Create Role button to launch steps for creating an IAM role.
Clicking Create Role button to launch steps for creating an IAM role.

2. On the Create role page, select the CodeDeploy option at the bottom as a common use case and click the Next: Permissions button at the bottom of the page. Selecting CodeDeploy grants the CodeDeploy the ability to call other AWS services on your behalf.

Locating the CodeDeploy common use case
Locating the CodeDeploy common use case
Selecting the CodeDeploy use case
Selecting the CodeDeploy use case

3. With the CodeDeploy common use case selected, the AWSCodeDeployRole is automatically attached to the new role. The CodeDeploy agent uses the AWSCodeDeployRole policy to make deployments to an EC2 instance on your behalf.

Showing the attached policies to the CodeDeploy use case
Showing the attached policies to the CodeDeploy use case

Click the Next: Tags button to proceed to the Tags step (no tags are necessary to assign), and then click the Next: Review button to proceed to the last step where you would review and name the new role.

4. Finally, specify a name, and description for this new role in the Role Name and Description input fields, then click the Create Role button to save the role.

  • Role name: CodeDeploy-Deployment-Role
  • Role description: Role used for CodeDeploy Deployment Group.
Reviewing details of CodeDeploy IAM Role
Reviewing details of CodeDeploy IAM Role

Creating an AWS EC2 Instance

With an IAM role created, you will now create a t2.micro instance running an Ubuntu-based Amazon Machine Image. You’ll create an EC2 instance to run the python-shortner-application to be deployed later in this tutorial. After creating the instance, you’ll also attach the IAM role you created to the instance.

1. Before creating the EC2 instance, you must first create a key pair. In the AWS Console, search for “key pairs” and click on Key pairs under Features.

Locating the EC2 Key Pairs feature
Locating the EC2 Key Pairs feature

2. Once there, click on Create key pair.

Creating a key pair
Creating a key pair

3. Enter the name, “PythonEC2KeyPair” and choose either .ppk (PuTTY) or .pem (OpenSSH) key format to download the Private Key in. Click on Create key pair and the private key will be immediately downloaded.

Entering in the key pair values
Entering in the key pair values
Downloading the private key for the key pair
Downloading the private key for the key pair

4. In a terminal, with a connected AWS CLI, run the create-security-group command below to create a security group for the EC2-Classic instance named, python-app-ec2-sg, which acts as a firewall.

aws ec2 create-security-group --group-name python-app-ec2-sg --description "Security group for EC2 instance running python application."
Creating the EC2 security group
Creating the EC2 security group

5. Now, execute the authorize-security-group-ingress command below to enable a rule for the security group you created, python-app-ec2-sg, previously.

The rule created by the command below enables incoming traffic from all sources (--cidr 0.0.0.0/0) that reach --port 8000 opened by python-shortner-application.

aws ec2 authorize-security-group-ingress --group-name python-app-ec2-sg --protocol tcp --port 8000 --cidr 0.0.0.0/0
Authorizing a rule for the EC2 security group
Authorizing a rule for the EC2 security group

Execute the command below to launch an EC2 instance (aws ec2 run-instances) that runs the python-shortner-application.

The EC2 instance uses the Ubuntu Server 20.04 LTS image, --image-id ami-00399ec92321828f5, and the security group, --security-groups python-app-ec2-sg, previously created to manage incoming traffic to the instance.

The ID supplied to the --image-id parameter in the command below belongs to the Ubuntu Server 20.04 LTS (HVM), SSD Volume Type Amazon Machine Image. You can execute the aws ec2 describe-images command to get a list of all available AMIs.

aws ec2 run-instances --image-id ami-00399ec92321828f5 --count 1 --instance-type t2.micro --key-name PythonEC2KeyPair --security-groups python-app-ec2-sg

Note the instance-id that the command returns as you’ll need it in the next step.

Retrieving the instance ID for the EC2 instance
Retrieving the instance ID for the EC2 instance

7. Execute the create-tag command below, using the InstanceId previously noted after the --resources parameter.

The tag added, --tags Key=used_by,Value=python-app, to the instance will later be used to match the instance when you create a deployment group within a CodeDeploy application.

aws ec2 create-tags --resources "i-0280eebb615ed3731" --tags Key=used_by,Value=python-app
Creating the necessary EC2 tags
Creating the necessary EC2 tags

8. Run the series of commands below to create an instance profile, add a role to that instance profile, and associate that instance profile to the EC2 instance

At this point, you’ve fully created the EC2 instance with a role containing all policies needed to use CodeDeploy and Azure Sessions Manager.

# Creates an  instance profile used to pass role, information to the EC2 instance 
aws iam create-instance-profile --instance-profile-name Python-app-ec2-instance-profile

# Adds the previously created instance profile to the python-app-ec2-codedeploy-role  
aws iam add-role-to-instance-profile --role-name python-app-ec2-codedeploy-role --instance-profile-name Python-app-ec2-instance-profile

# Associates the instance profile to the EC2 instance 
aws ec2 associate-iam-instance-profile --instance-id "i-0280eebb615ed3731" --iam-instance-profile Name=Python-app-ec2-instance-profile
Creating the instance profile and adding the instance profile role
Associating the IAM instance profile to the role
Associating the IAM instance profile to the role

9. Finally, in the AWS console, view the summary of the EC2 instance you have created.

Viewing the summary of the EC2 instance
Viewing the summary of the EC2 instance

Retrieving the python-shortner-application Code for Deployment

Although you will have your own application to deploy, in this tutorial an example Python application is provided for testing. A completed version of this sample Python application called python-shortner-application, including the deployment files, can be found in the linked GitHub repository.

The python-shortner-application exposes a GET and POST endpoint for shortening a URL and is pre-built using the FastAPI Web Framework. FastAPI is a highly performant framework for building Python APIs.

Below, you can see the files that come along with the python-url-shortner folder.

  • A Dockerfile and docker-compose.yml file – These two files contain the configurations needed for building a container of this application in Docker. The docker-compose.yml file contains two services: a database service for the MongoDB database and the Python API service for the python-shortner-application. MongoDB is a document-based and open-source NoSQL database used for high-volume data storage.
  • A routes.py file – Utilizes the FastAPI client class to create a GET and POST API endpoint for shortening a URL.
  • A requirements.txt file – Contains a list of all Python packages installed within the virtual environment created for python-shortner-application.

Before moving on and uploading these files to an AWS S3 bucket for deployment (where AWS CodeDeploy pulls source files from), you must first download the files or clone the ATA Scripts repository and use the files in the python-url-shortner directory.

# Clone the ATA Scripts Repository
git clone https://github.com/Adam-the-Automator/Scripts.git
# Change to the python-url-shortner directory
cd .\Scripts\python-url-shortner\
# List the files in the directory
ls
Checking out the GIT repository
Checking out the GIT repository

If you have Docker locally installed, you can test the application locally by running docker-compose up in the python-url-shortner directory.

Uploading Source Code To An AWS S3 Storage Bucket

Before the code can be deployed, the code must first be uploaded and stored in an AWS S3 bucket. The source code will serve as the source for the two deployments you will perform within this tutorial.

1. In a terminal, with a connected AWS CLI, run the following s3api create-bucket command to generate the necessary bucket to store the code. In this example, the bucket is named python-app-bucket set to be publicly readable via the public-read ACL and configured to be set in the same region as your EC2 instance.

Since bucket names are shared across all users of AWS, it must be unique. In this case, the date has been appended for uniqueness, but you will have to change yours appropriately.

aws s3api create-bucket --bucket python-app-bucket-10212021 --acl public-read --create-bucket-configuration LocationConstraint=us-east-2
Creating the AWS S3 bucket
Creating the AWS S3 bucket

2. Next, execute the tar command below to compress the files for python-shortner-application located in the codedeploy-files sub-directory into a GZIP TAR file named python-app.tar.gz. CodeDeploy will download and extract the file on each deployment.

cd .\codedeploy-files\
tar -czvf python-app.tar.gz .
Creating the GZipped TAR file
Creating the GZipped TAR file

3. Finally, execute the cp command below to upload the compressed file (python-app.tar.gz) into the previously created AWS S3 bucket (s3://python-app-bucket-10212021).

aws s3 cp python-app.tar.gz s3://python-app-bucket-10212021
Uploading the generated GZipped TAR file to AWS S3
Uploading the generated GZipped TAR file to AWS S3

Creating an AppSpec.yml and Additional Deployment Files for AWS CodeDeploy

At this point, you now have a good understanding of the necessary setup and prerequisites. Now it’s time to focus on deploying the example application into production using CodeDeploy. But first, you’ll need to create AppSpec and the container start and stop script files.

1. With your preferred code editor, create an appspec.yml file in the root directory of python-shortner-application folder. The appspec.yml file will contain the AppSpec configurations for CodeDeploy.

This is all provided in the linked GitHub directory, but covered in this tutorial as well.

When the three lifecycle event hooks defined in the code below are executed individually during the deployment process, they will install Docker and docker-compose. Docker and docker-compose are further used to build a Docker image of the application on the EC2 instance.

# The deployment version, with a default value of 0.0, CodeDeploy will automatically increment this value on each successful deployment
version: 0.0

# The operating system running on the target compute resource.
# For EC2 instances using Ubuntu, RHEL, or Linux Amazon Machine Images (AMI) use Linux as the value.
os: linux

# A list of files to copy on each deployment.
files:
  # A directory to copy in each application revision, if only a "/" is used for the source, all files in that directory are copied.
  - source: /
    destination: /app

# The "hooks" section defines the scripts to be run on each Lifecycle event.
# The hook events are run in the following order: "BeforeInstall", "ApplicationStop", "ApplicationStart", "ValidateService"
hooks:
  # Prior to installation steps, run the following actions.
  BeforeInstall:
    # The location for the "install_docker.sh" script to install Docker and docker-compose.
    - location: install_docker.sh
      # Run the CodeDploy agent with root permissions.
      runas: root

  # Run the lifecycle event to stop the application via the defined script.
  ApplicationStop:
    - location: stop.sh
      timeout: 2000
      runas: root

  # Run the lifecycle event to start the application via the defined script.
  ApplicationStart:
    - location: start.sh
      timeout: 3600
      runas: root

2. Now create an install_docker.sh file in the root directory of python-shortner-application, and copy the code below into the install_docker.sh file. The install_docker.sh file is referenced to the BeforeInstall event in the previous code inside the appspec.yml.

The code below updates the apt repository and installs all packages needed for installing Docker and Docker Compose on the host virtual machine.

#!/bin/bash

# Update container Linux image APT repository data
apt-get update

# Install Docker required packages
apt-get -y install apt-transport-https ca-certificates lsb-release

# Add the Docker GPG key to trust the Docker APT repository
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Update container Linux image APT repository data again after adding the Docker repository
apt-get update

# Install the Docker community edition and cli
apt-get -y install docker-ce docker-ce-cli containerd.io

# Download docker-compose executable file, replace the 1.29.2 listed below with the latest version you intend to use.
curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

# Change permission of docker-compose to become executable
chmod +x /usr/local/bin/docker-compose

3. Within the root directory, create another file named start.sh. Copy the code below and paste it into the start.sh file. The start.sh file is referenced to the ApplicationStart lifecycle event in the appsec.yml file.

The code below changes the directory into the app folder where the application revision is copied to and creates a container image of the application each time a new application deployment is made.

#!/bin/bash

cd /app

docker-compose up --build -d

The -d parameter instructs docker-compose to run in detached mode, the background, and to always build (--build) the container before running the container.

4. Finally, create a file in the root directory named stop.sh, and add the code below into that file. Like the previous files you created, the stop.sh file is referenced to the ApplicatonStop lifecycle event in the appsec.yml file.

When the code below is executed, it changes the directory into the app folder and runs the docker-compose command to stop (down) the running compose container.

#!/bin/bash

cd /app

docker-compose down

Deploying Applications

With an EC2 instance created while having the python-app-ec2-codedeploy-role attached, it is time to deploy the python-shortner-application to the EC2 instance via CodeDeploy.

To deploy the python-shortner-application, you’ll first need to create an application on the CodeDeploy service.

1. Execute the create-application command below from your terminal to create an application on the CodeDeploy service (--application-name python-url-codedeploy-app). The created application will make deployments to your EC2 instance ( --compute-platform Server ).

Notice a tag is also added to identify the EC2 instance ( -tags Key=user,Value=python-url-shortner).

aws deploy create-application --application-name python-url-codedeploy-app --compute-platform Server --tags Key=user,Value=python-url-shortner
Creating the CodeDeploy application
Creating the CodeDeploy application

2. Retrieve the role ARN value for use in the CodeDeploy command.

aws iam get-role --role-name CodeDeploy-Deployment-Role
Retrieving the ARN values for the role
Retrieving the ARN values for the role

3. Execute the create-deployment-group command to create a deployment group (--deployment-group-name python-url-app-deploygroup). The command creates the deployment group within the created CodeDeploy application earlier (--application-name python-url-codedeploy-app).

An Amazon Resource Name ( ARN ) is used to identify an AWS resource, and it consists of your AWS Account ID (aws:iam::591107461920) and the role name (role/CodeDeploy-Deployment-Role).

Your AWS Account ID will differ from the one shown here, make sure to replace with yours.

aws deploy create-deployment-group --deployment-group-name python-url-app-deploygroup --application-name python-url-codedeploy-app --service-role-arn arn:aws:iam::591107461920:role/CodeDeploy-Deployment-Role --ec2-tag-filters Key=used_by,Value=python-app,Type=KEY_AND_VALUE
Creating the application deployment group

Creating the application deployment group

4. Next, execute the create-deployment command below to create a deployment of the CodeDeploy application (--application-name python-url-codedeploy-app) from an S3 bucket (--s3-location bucket=python-app-bucket-10212021), your bucket will be named differently.

The command below also specifies the deployment group ( --deployment-group-name python-dg), and bundle type of the files ( bundleType=tgz )

aws deploy create-deployment --application-name python-url-codedeploy-app --deployment-group-name python-url-app-deploygroup --s3-location bucket=python-app-bucket-10212021,bundleType=tgz,key=python-app.tar.gz
Creating the application deployment
Creating the application deployment

5. In the AWS console, enter “CodeDeploy” into the search input field, and choose CodeDeploy. Doing so redirects the page to the CodeDeploy service section.

Searching for CodeDeploy service through AWS console
Searching for CodeDeploy service through AWS console

From the CodeDeploy section of the AWS Console, you can view the deployment status created above.

Viewing lifecycle events executed from a deployment made by CodeDeploy
Viewing lifecycle events executed from a deployment made by CodeDeploy

6. Now make a POST request to the instance through the public IPv4 address to confirm that python-shortner-application runs within the EC2 instance.

Execute the describe-instances command below to list all EC2 instances (ec2) created within your AWS Account.

aws ec2 describe-instances

You can see below the command returns the PublicIpAddress of the EC2 instance. Note the PublicIpAddress as you’ll need it in the next step.

Viewing the PublicIpAddress for EC2 instance
Viewing the PublicIpAddress for EC2 instance

7. Finally, execute the curl command below to make a POST request to the instance from your local terminal to shorten the URL you specified. Replace the PUBLIC-IP placeholder with the PublicIpAddress you took note of previously (step five).

curl -X POST -H "Content-Type: application/json" http://PUBLIC-IP:8000/shorty -d '{"url":"https://docs.aws.amazon.com"}'

The command returns a short URL in JSON format, as shown below.

Shortening a URL
Shortening a URL

Redeploying the API using AWS CodeDeploy

With the python-shortner-application deployed to the EC2 instance, let’s modify the application to learn how CodeDeploy applies new changes in the application to the EC2 Instance.

You’ll make a slight change to the python-shortner-application, upload the new changes to the storage bucket, and apply the changes to the EC2 instance.

1. In your code editor, open the routes.py file within the python-shortner-application, and add the code below into the routes.py file.

The code below contains a GET endpoint handler that returns a JSON response containing details about the application.

# Create GET /detail endpoint
@app.get("/detail")
async def detail():
		# return string describing entire API
		return {"message":"REST API for generating short URLs"}

2. Execute the commands below to delete the previous zip folder, create a new one, and upload it to the S3 bucket.

# Remove the old ZIP file
rm -rf python-app.tar.gz

# Create new ZIP file, excluding the .git VCS directory 
# and python virtual environment ( venv ) 
tar -czvf python-app.tar.gz . --exclude=/url/venv --exclude=/url/.git

# Upload the new ZIP file to the AWS S3 Bucket
aws s3 cp python-app.tar.gz s3://python-app-bucket

3. Now run the create-deployment command below to trigger a redeployment of the CodeDeploy application (python-url-codedeploy-app) within the deployment group ( python-dg ). Doing so will pull the new code from the S3 bucket (--s3-location bucket=python-app-bucket-10212021) apply the new code changes to the running container within the EC2 instance.

aws deploy create-deployment --application-name python-url-codedeploy-app --deployment-group-name python-dg --s3-location bucket=python-app-bucket-10212021,bundleType=tgz,key=python-app.tar.gz

4. Click the Deployments item within the Deploy dropdown in the console sidebar to navigate to the Deployments section.

In the Deployments section, you’ll see the list of all deployments made via CodeDeploy. The list includes your recent deployment to apply the code changes uploaded to the AWS S3 bucket, as shown below.

Viewing CodeDeploy Deployments History
Viewing CodeDeploy Deployments History

5. Finally, execute the curl command below to test the new endpoint.

The command sends a GET request to the python-shortner-application running within the EC2 instance. Replace the PUBLIC_IPV4_ADDRESS placeholder in the command below with the public IPV4 address of your EC2 instance.

curl http://PUBLIC_IPV4_ADDRESS:8000/detail

The response returned by the curl command contains the details of the REST API, as shown below. The details indicate that the new change to the API was deployed successfully by CodeDeploy to the EC2 instance.

With this setup in place, you can make more changes to the python-shortner-application, make an application revision. Apply new changes to the EC2 instance by triggering a deployment from CodeDeploy as done previously (step three).

Executing a GET request to redeploy the application
Executing a GET request to redeploy the application

Conclusion

This tutorial aimed to teach you how to automate deployments to an EC2 instance on AWS by using CodeDeploy. Did it succeed? In this tutorial, you learned about CodeDeploy, and you’ve put together an AppSpec configuration file to organize the deployment process.

With this newfound knowledge, why not learn how you can use AWS Lambda functions and Amazon S3 buckets to automate actions in CodeDeploy further?

Hate ads? Want to support the writer? Get many of our tutorials packaged as an ATA Guidebook.

Explore ATA Guidebooks

Looks like you're offline!