How to sync containers to AWS ECS the easy way

It's actually really easy to set this up using CodeBuild & Event Bridge all wrapped up in a Cloudformation Stack!

How to sync containers to AWS ECS the easy way
Photo by Antoine Petitteville / Unsplash

Back in November 2021 AWS announced that you can cache containers from DockerHub through to ECS.

Announcing Pull Through Cache Repositories for Amazon Elastic Container Registry

However it comes with this little fine print which in my situation it's just not going to float.

The solution

It's actually really easy to set this up using CodeBuild & Event Bridge all wrapped up in a Cloudformation Stack!

Here are resources/services we will be working with.

  • A nightly Event Bridge rule will trigger the CodeBuild Project
  • Codebuild project will perform the interaction with DockerHub for the target container image and sync the latest 5 tags

The Cloudformation Stack

The stack requires two parameters

  • ImageRepoName - DockerHub Image Repo name hashicorp/terraform
  • ImageTagCount - Number of tags to retrive 2
---
AWSTemplateFormatVersion: 2010-09-09
Description: Creates a CodeBuild project to sync containers from DockerHub

Parameters:

  ImageRepoName:
    Description: 'Target Image Repo, this name should be identical to the one hosted on DockerHub'
    Type: String
    Default: 'hashicorp/terraform'

  ImageTagCount:
    Description: 'The number of tags to keep updated, simple head from curl output to query tags on DockerHub'
    Type: Number
    Default: 2

Resources:
  Repository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: !Ref ImageRepoName
      ImageScanningConfiguration:
        ScanOnPush: true

  CodeBuild:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Join
        - '-'
        - - !Ref 'AWS::StackName'
          - CodeBuild
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          -
            Action: 'sts:AssumeRole'
            Effect: Allow
            Principal:
              Service:
                - codebuild.amazonaws.com
      Policies:
        - PolicyName: ECR
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Action:
                  - ecr:BatchCheckLayerAvailability
                  - ecr:CompleteLayerUpload
                  - ecr:GetAuthorizationToken
                  - ecr:InitiateLayerUpload
                  - ecr:PutImage
                  - ecr:UploadLayerPart
                Effect: Allow
                Resource: '*'
        - PolicyName: LogGroup
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Effect: Allow
                Resource: !Sub 'arn:aws:logs:ap-southeast-2:${AWS::AccountId}:log-group:/aws/codebuild/*'

  DockerHubSync:
    Type: AWS::CodeBuild::Project
    Properties:
      Artifacts:
        Type: NO_ARTIFACTS
      Source:
        Type: NO_SOURCE
        BuildSpec: |
          version: 0.2

          phases:
            pre_build:
              commands:
                - echo "Logging in to Amazon ECR..."
                - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
            build:
              commands:
                - echo "Retrieving Docker image tags..."
                - TAGS=$(curl -L -s https://registry.hub.docker.com/v2/repositories/${IMAGE_REPO_NAME}/tags | jq '."results"[]["name"]' | head -n${IMAGE_TAG_COUNT} | sed s'/"//g')
                - for TAG in $TAGS; do docker pull $IMAGE_REPO_NAME:$TAG;done
                - for TAG in $TAGS; do docker tag $IMAGE_REPO_NAME:$TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$TAG;done
            post_build:
              commands:
                - echo "Pushing image tags to ECR..."
                - for TAG in $TAGS; do docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$TAG;done
      Environment:
        ComputeType: "BUILD_GENERAL1_SMALL"
        Image: "aws/codebuild/standard:5.0"
        Type: "LINUX_CONTAINER"
        PrivilegedMode: true
        EnvironmentVariables:
          - Name: AWS_DEFAULT_REGION
            Type: PLAINTEXT
            Value: "ap-southeast-2"
          - Name: AWS_ACCOUNT_ID
            Type: PLAINTEXT
            Value: !Ref AWS::AccountId
          - Name: IMAGE_TAG_COUNT
            Type: PLAINTEXT
            Value: !Ref ImageTagCount
          - Name: IMAGE_REPO_NAME
            Type: PLAINTEXT
            Value: !Ref ImageRepoName
      Description: !Sub 'Run ${ImageRepoName} image tags repo sync from Dockerhub to local ECR repo'
      ServiceRole: !GetAtt CodeBuild.Arn
      TimeoutInMinutes: 300

  LogGroup:
    Type: 'AWS::Logs::LogGroup'
    Properties:
      LogGroupName: !Sub '/aws/codebuild/${DockerHubSync}-${ImageRepoName}'
      RetentionInDays: 3

  EventRole:
    Type: AWS::IAM::Role
    Properties:
      Description: IAM role to allow Amazon CloudWatch Events to trigger AWS CodeBuild build
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: events.amazonaws.com
      Policies:
        - PolicyDocument:
            Statement:
              - Action: codebuild:StartBuild
                Effect: Allow
                Resource: !GetAtt 'DockerHubSync.Arn'
          PolicyName: !Join
            - '-'
            - - !Ref 'AWS::StackName'
              - CloudWatchEventPolicy
      RoleName: !Join
        - '-'
        - - !Ref 'AWS::StackName'
          - CloudWatchEventRule

  NightlyEvent:
    Type: AWS::Events::Rule
    Properties:
      Description: Rule for Amazon CloudWatch Events to trigger a build every night
      ScheduleExpression: "cron(0 0 * * ? *)"
      Name: !Join
        - '-'
        - - !Ref 'AWS::StackName'
          - NightlyBuild
      State: ENABLED
      Targets:
        - Arn: !GetAtt 'DockerHubSync.Arn'
          Id: NightlyCheck
          RoleArn: !GetAtt 'EventRole.Arn'

Once the stack has been created, the codebuild job will kick off on the schedule and perform the sync.

The output will look something like this.

[Container] 2022/06/01 00:01:30 Running command echo "Retrieving Docker image tags..."
Retrieving Docker image tags...

[Container] 2022/06/01 00:01:30 Running command TAGS=$(curl -L -s https://registry.hub.docker.com/v2/repositories/${IMAGE_REPO_NAME}/tags | jq '."results"[]["name"]' | head -n${IMAGE_TAG_COUNT} | sed s'/"//g')

[Container] 2022/06/01 00:01:32 Running command for TAG in $TAGS; do docker pull $IMAGE_REPO_NAME:$TAG;done
latest: Pulling from hashicorp/terraform
2408cc74d12b: Pulling fs layer
86c08b1682e5: Pulling fs layer
89232a3d66c3: Pulling fs layer
2408cc74d12b: Verifying Checksum
2408cc74d12b: Download complete
86c08b1682e5: Verifying Checksum
86c08b1682e5: Download complete
89232a3d66c3: Verifying Checksum
89232a3d66c3: Download complete
2408cc74d12b: Pull complete
86c08b1682e5: Pull complete
89232a3d66c3: Pull complete
Digest: sha256:8aecb79b6197269722e2e3fba5040756135d20427da395a0f9883bb0927e16d1
Status: Downloaded newer image for hashicorp/terraform:latest
docker.io/hashicorp/terraform:latest
light: Pulling from hashicorp/terraform
Digest: sha256:8aecb79b6197269722e2e3fba5040756135d20427da395a0f9883bb0927e16d1
Status: Downloaded newer image for hashicorp/terraform:light
docker.io/hashicorp/terraform:light

And here are our containers, in our private repo for consumption

Hope this helps someone else

Cheers!