Assignment 4

Continuous Integration and Continuous Delivery

Due Saturday, March 9 at 11:59pm

For this assignment, you will build an end to end CI/CD pipeline using GitHub Actions and AWS Elastic Container Registry (ECR) that results in all commits to the Yoctogram backend being immediately deployed to the Fargate cluster. Specifically, we will create an ECR repository, configure GitHub Actions to push new images to this repository, modify the CDK to pull from this ECR repository, and automatically redeploy the Fargate cluster.

As with previous assignments, all the assignment work should take place on your development EC2 virtual machine since it has all the tools you need. This assignment builds on Assignment 2; however, it can be done independently of Assignment 3.

Creating the ECR Repository

Run the command

aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin ACCOUNTID.dkr.ecr.us-west-2.amazonaws.com
aws ecr create-repository --repository-name cs40

to create the ECR repository we’ll use for the rest of the assignment. This repository will be used to store the container images used to host Yoctogram. You can verify the repository was created successfully by navigating to ECR in the AWS console and verifying the cs40 repo is there.

Push an Initial Image to ECR

While not strictly necessary, this section will prevent errors (some of which may be difficult to recover from) when deploying the CDK. Note that all of the work here will eventually have to be done in the GitHub Action.

Navigate to the top level app directory (the one containing a Dockerfile), in the same tree you’ve used to complete assignments 2 and 3. You need to build the Docker image from the Dockerfile and, more importantly, you must tag the image using a specific format. The tag must look like:

ECR_REGISTRY/ECR_REPOSITORY:latest

where ECR_REGISTRY is the url ACCOUNTID.dkr.ecr.us-west-2.amazonaws.com (ACCOUNTID is your twelve digit AWS account ID) and ECR_REPOSITORY is the name of the repository you created earlier (cs40 if you named it as this spec does).

An example might look like

123456789012.dkr.ecr.us-west-2.amazonaws.com/cs40:latest

Once you have built the docker image, you can push it to ECR with the following command:

docker push ECR_REGISTRY/ECR_REPOSITORY:latest

where ECR_REGISTRY and ECR_REPOSITORY are as earlier. Now, you should be able to navigate to the ECR repository through the AWS console and see your Docker image tagged latest. It should be around 105 MB. If the image is not there, you made a mistake and should double check your commands. If it is, continue to the next section.

ℹ️

It is convention that the most recent image should be called latest. However, using the same syntax, you can tag images any way you prefer. For example,

ECR_REGISTRY/ECR_REPOSITORY:foo

would push a new image tagged as foo to ECR.

Clone Yoctogram Backend Code

First, import the Yoctogram backend repository into a private repository. Log into GitHub and click “Import repository”, as you did for Assignment 2.

Use https://github.com/infracourse/yoctogram-app for “Your old repository’s clone URL”. You can name the new repository anything under your personal account; we suggest something like yoctogram-app. Remember the repo name as it will be used in the CDK later.

Make sure to keep the repository private. If you’re working with a partner, make sure to add them as a collaborator in Settings.

On your EC2 instance, clone the Yoctogram backend code on the same level as your assignment 2 repo (not inside of it): git clone --recursive [email protected]:yourusername/yoctogram-app.

Modify CDK to Pull from ECR and Configure GitHub OIDC

First, in compute_stack.py add

aws_ecr as ecr,
aws_iam as iam

to the existing long list of imports from aws_cdk.

Pulling from ECR

There are two changes you should make to your ECS Fargate task definition. Both changes should be at most a few lines of code.

First, you need to import the container registry from the one you created earlier. To do so, you can use the ecr.Repository.from_repository_name CDK construct.

Then, in the Yoctogram app container, where you previously used from_asset to import the container image from the local filesystem, you should instead import the ECS container image from the ECR repository. Use the latest tag.

Configuring GitHub OIDC

For the latter part of this assignment, GitHub will need the ability to authenticate to your AWS account in order to push images to ECR and redeploy the compute stack.

ℹ️
Historically, this was done using long lived credentials, which posed a major security risk because any GitHub compromise could result in your AWS environment also being compromised (see the lecture from February 21 on CI/CD). OpenID Connect (OIDC) allows us to specify temporary access credentials which are discarded after every use.

First, you will need to configure the GitHub OIDC provider. To do this, paste the following block at the bottom of your compute stack code:

gh_actions_provider = iam.OpenIdConnectProvider(
    self,
    f"{settings.PROJECT_NAME}-gh-actions-provider",
    url="https://token.actions.githubusercontent.com",
    client_ids=["sts.amazonaws.com"],
)

Now, you will need to configure an IAM role to be assumed by GitHub. Configure the role to be assumed by a WebIdentityPrincipal specifying the Amazon Resource Name (ARN) of the GitHub OIDC provider. You’ll also have to specify

{
  "token.actions.githubusercontent.com:sub": "repo:your-github-account/your-yoctogram-app-repo:ref:refs/heads/main",
  "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
}

as StringLike conditions for the principal. This limits which repositories have access to the role to just your Yoctogram backend repository.

Attach the AmazonEC2ContainerRegistryFullAccess and AmazonECS_FullAccess managed policies (policies created by AWS) to the role, and ensure a session length of one hour maximum.

A grade4.sh script to allow you to check your work will be made available shortly. However, if at this stage you made these modifications, can redeploy the compute stack, and the deploy completes successfully, then the ECR configuration likely worked. If something is wrong with your role, it will be obvious in the following stage because you’ll get errors about being unable to assume a role or about insufficient permissions.

Configuring GitHub Actions to Push to ECR

First, allow actions in your GitHub repository by navigating to Settings in your repository’s menu, and selecting General under Actions. Ensure that Allow all actions and reusable workflows is selected.

Then, add a GitHub Repository Secret to your repo with the name AWS_ACCOUNT_ID and the value as your 12-digit AWS account ID. Looking at your repository in a browser, you can do this by clicking Settings –> Secrets and variables –> Actions (Settings is in the top bar alongside Code, Issues, Pull Requests, etc).

Now, copy this starter code to .github/workflows/main.yml in the repository you just cloned.

name: CI/CD Pipeline

on:
  push:
    branches: [ main ]

permissions: write-all

env:
  AWS_REGION: us-west-2
  ECR_REGISTRY: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.us-west-2.amazonaws.com
  ECR_REPOSITORY: cs40

jobs:
  build-and-push:
    runs-on: ubuntu-latest

    steps:
    # COMPLETED FOR YOU
    - name: 1 Retrieve Timestamp
      id: timestamp
      run: echo "::set-output name=timestamp::$(date +%s%3N)"

    # COMPLETED FOR YOU
    - name: 2 Checkout code
      uses: actions/checkout@v2

    # FILLMEIN: replace ROLE_NAME_HERE with the name of the role you defined in CDK
    - name: 3 Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/ROLE_NAME_HERE
        aws-region: us-west-2

    # COMPLETED FOR YOU
    - name: 4 Login to Amazon ECR
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v1

    # COMPLETED FOR YOU
    - name: 5 Set up Docker Buildx
      uses: docker/setup-buildx-action@v1

    # FILLMEIN: configure the action to build the docker image but *NOT* push it to ECR
    - name: 6 Build Docker images
      uses: docker/build-push-action@v2
      with:

    # FILLMEIN: Remove the latest tag from all existing images
    - name: 7 Remove latest Tag From Old latest
      run: COMMAND_HERE

    # FILLMEIN: Push to ECR
    - name: 8 Push Docker images
      uses: docker/build-push-action@v2
      with:

    # FILLMEIN: populate the correct values for cluster name and service
    # This is a hack for the purposes of this assignment. Don't do this in production.
    - name: 9 Force a Fargate Redeploy
      run: aws ecs update-service --cluster <cluster-name> --service <service-name> --force-new-deployment --region us-west-2

Note that .github/workflows is a folder containing YAML files, each of which represent an independent GitHub action.

  • Step 3 should use the name of the role you defined earlier in CDK. You can find this in your AWS console –> IAM –> Roles (note, the list has multiple pages, so you may have to click through to the second page).
  • Step 6 should build and tag the Docker images as you did earlier in the assignment, except instead of using raw Docker commands it should use the Docker GitHub action (you just need to populate the with: fields properly). You should include two tags – latest and the timestamp retrieved earlier. You can reference the timestamp using ${{ steps.timestamp.outputs.timestamp }}
    ℹ️
    Make sure to specify the platform as linux/arm64 and to not push the images.
  • Step 7 should use the AWS command-line interface (CLI) to remove the tag latest from all existing images, so that when we push our image we don’t have duplicates.
  • Step 8 should be identical to step 6, except it should specify push: true.
  • Step 9 is done for you except it needs two values, the name of your ECS cluster and the name of the task definition (this is the --service flag). Both of these values are likely very long. For Cody the full command is
    aws ecs update-service \
      --cluster yoctogram-compute-stack-yoctogramcluster2BA83DB9-HZYMbQe0NRT0 \
      --service yoctogram-compute-stack-yoctogramfargateserviceServiceDB9282DF-ajLuzi20W8Sk 
      --force-new-deployment \
      --region us-west-2

To verify if your GitHub action worked, make a commit and verify that when the workflow is complete, there is a new container image in your ECR repo and there is a task in your ECS cluster indicating a redeploy.

Submission and Grading

Local Sanity Check

Copy and paste the following file into your assignment2 root directory. Call the file grade4.sh, and mark it executable by running chmod +x grade4.sh:

#!/bin/bash

set -euxo pipefail

echo "RUN THIS SCRIPT FROM YOUR assignment2 ROOT DIRECTORY. MAKE SURE YOU HAVE SOURCED YOUR venv"

pip3 install pyyaml jinja2

pushd cdk
cdk synth
popd

git clone https://github.com/infracourse/assignment4-autograder autograder

opa eval -b autograder/rules/ -i <(jq -s 'reduce .[] as $item ({}; .Resources += $item.Resources) | del(.Resources.CDKMetadata)' cdk/cdk.out/yoctogram-network-stack.template.json cdk/cdk.out/yoctogram-data-stack.template.json cdk/cdk.out/yoctogram-compute-stack.template.json) -f json 'data.rules.main' | jq -r .result[].expressions[].value.violations[]

python3 autograder/grade_action.py $1

rm -rf autograder

When you run this script, make sure to activate your Python virtual environment, and pass in the path to your GitHub Actions workflow YAML file. For example, one run might look like

./grade4.sh ~/yoctogram-app/.github/workflows/main.yml

If there are no errors, you should be good to go and you should receive a full score when submitting. There are no additional checks against your live site.

Submission

To submit, copy the .github/workflows/main.yml file from your yoctogram-app directory to the assignment2 root directory (i.e., the one with the SUNET and possibly FLAG files) and push this to GitHub. Upload your submission from GitHub to Gradescope as usual.

The filestructure should look like:

.
├── app/
├── cdk/
├── cdk.out/
├── compression/
├── FLAG
├── grade.sh
├── grade3.sh
├── grade4.sh               # create this file by copying the script above
├── main.yml                # copy the action from your private github repository
├── README.md
├── SUNET
└── web/

The point distribution for this assignment is as follows:

  • 100 points: Static Open Policy Agent evaluation against our OPA policies to ensure your CDK (synthesized to CloudFormation) meets our specification.
  • 50 points: Static checks on your GitHub Actions YAML file to ensure it meets our specification.

All checks are locally runnable using grade4.sh.