リポジトリポリシーを利用してCodePipeline内のECRへの手動プッシュを防止する

リポジトリポリシーを利用してCodePipeline内のECRへの手動プッシュを防止する

リポジトリポリシーを利用して、CodePipeline内のECRへの手動プッシュを防止する

以下のページで、ECRリポジトリポリシーについて取り上げました。

https://awstut.com/2022/09/24/introduction-to-ecr-repository-policies-using-cfn

本ページでは、リポジトリポリシーを使用して、CodePipeline内のECRリポジトリへの手動プッシュ/プルを防止することを目標とします。

構築する環境

Diagram of use repository policies to prevent manual pushes to the ECR in CodePipeline

CodePipelineを構成し、3つのリソースを連携させます。

1つ目はCodeCommitです。
CodeCommitはCodePipelineのソースステージを担当します。
Gitリポジトリとして使用します。

2つ目はCodeBuildです。
CodeBuildはCodePipelineのビルドステージを担当します。
CodeCommitにプッシュされたコードから、Dockerイメージをビルドします。
ビルドしたイメージをECRにプッシュします。

3つ目はLambda関数です。
この関数の働きは、ECS(Fargate)サービスのタスク希望数を変更することです。
具体的には希望数を0から1に変更します。

CodePipelineにデプロイステージを作成します。
後述のFargateにデプロイするように設定します。

SSMパラメータストアにDockerHubアカウント情報を保存します。
DockerBuildでイメージを生成する際に、DockerHubにサインインした上でベースイメージをプルするために、これらを使用します。

CodePipelineが開始されるきっかけですが、CodeCommitへのプッシュを条件とします。
具体的には、EventBridgeに上記を満たすルールを用意します。

プライベートサブネットに、FargateタイプのECSを作成します。

ECRリポジトリにリポジトリポリシーを設定します。

  • CodeBuild用IAMロール:イメージのプッシュを許可する
  • ECSタスク実行用IAMロール:イメージのプルを許可する
  • それ以外のリソース:全てのアクションを拒否する

EC2インスタンスを作成します。
Fargate上に作成されたコンテナにアクセスするためのクライアントして使用します。
加えて、ECRに関するフルアクセス権限を付与します。

CloudFormationテンプレートファイル

上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。

https://github.com/awstut-an-r/awstut-fa/tree/main/084

テンプレートファイルのポイント解説

本ページはリポジトリポリシーを使用して、CodePipeline内のECRリポジトリへの手動プッシュ/プルを防止する方法を取り上げます。

CodePipelineに関する基本的な事項については、以下のページをご確認ください。

あわせて読みたい
CodePipelineを使ってCodeCommitプッシュをトリガーにしてECRにイメージをプッシュする 【CodePipelineを使ってCodeCommitプッシュをトリガーにしてECRにイメージをプッシュする】 CodePipelineを使用することによって、CI/CD構成を構築することができます。...

CodePipelineにデプロイステージを作成し、ECS(Fargate)にデプロイする方法については、以下のページをご確認ください。

あわせて読みたい
CodePipelineでイメージをビルドしてFargateにデプロイする 【CodePipelineでイメージをビルドしてFargateにデプロイする】 以下のページで、CodePipelineを使ってパイプラインを構成して、CodeCommitとCodeBuildを連携させて、EC...

CodePipeline内でLambda関数を呼び出すアクションを定義し、ECS(Fargate)サービスの希望数を変更する方法については、以下のページをご確認ください。

あわせて読みたい
CodePipelineにLambda関数を呼び出すアクションを定義して、Fargateタスクの希望数を変更する 【CodePipelineにLambda関数を呼び出すアクションを定義して、Fargateタスクの希望数を変更する】 CodePipelineにはさまざまなアクションを設定できますが、今回はパイ...

プライベートサブネットにFargateを構築する方法については、以下のページをご確認ください。

あわせて読みたい
プライベートサブネットにECS(Fargate)を作成する 【プライベートサブネットにECS(Fargate)を作成する】 以下のページでFargateタイプのECSコンテナを作成する方法をご紹介しました。 https://awstut.com/2022/01/25/int...

CloudFormationカスタムリソースを使用して、CloudFormationスタック削除時に、自動的にS3バケット内のオブジェクトと、ECRリポジトリ内のイメージを削除します。
詳細につきましては、以下のページをご確認ください。

あわせて読みたい
CFNカスタムリソースでS3オブジェクトを作成・削除する 【CloudFormationカスタムリソースを使って、スタック生成/削除時にS3オブジェクトを作成/削除する方法】 CloudFormationカスタムリソースはスタック操作(作成、更新、...
あわせて読みたい
CFNカスタムリソースでECRイメージを削除する 【CloudFormationカスタムリソースでECRイメージを削除する】 CloudFormationを使ってECRを作成し、そこにイメージをプッシュすると、CloudFormationスタック時にエラー...

IAMロール

CodeBuild用IAMロール

Resources:
  CodeBuildRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - codebuild.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser
      Policies:
        - PolicyName: PipelineExecutionPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ssm:GetParameters
                Resource:
                  - !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${SSMParameterDockerHubPassword}"
                  - !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${SSMParameterDockerHubUsername}"
              - Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketAcl
                  - s3:GetBucketLocation
                Resource:
                  - !Sub "arn:aws:s3:::${BucketName}"
                  - !Sub "arn:aws:s3:::${BucketName}/*"
Code language: YAML (yaml)

AWS管理ポリシーAmazonEC2ContainerRegistryPowerUserをアタッチして、ECRリポジトリへのプッシュ/プル等を許可します。
ちなみに本ポリシーの内容は以下の通りです。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:GetRepositoryPolicy",
                "ecr:DescribeRepositories",
                "ecr:ListImages",
                "ecr:DescribeImages",
                "ecr:BatchGetImage",
                "ecr:GetLifecyclePolicy",
                "ecr:GetLifecyclePolicyPreview",
                "ecr:ListTagsForResource",
                "ecr:DescribeImageScanFindings",
                "ecr:InitiateLayerUpload",
                "ecr:UploadLayerPart",
                "ecr:CompleteLayerUpload",
                "ecr:PutImage"
            ],
            "Resource": "*"
        }
    ]
}
Code language: JSON / JSON with Comments (json)

ECSタスク実行用IAMロール

Resources:
  FargateTaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ecs-tasks.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
Code language: YAML (yaml)

AWS管理ポリシーAmazonECSTaskExecutionRolePolicyをアタッチして、ECRリポジトリへのプル等を許可します。
ちなみに本ポリシーの内容は以下の通りです。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:BatchGetImage",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}
Code language: JSON / JSON with Comments (json)

EC2インスタンス用IAMロール

Resources:
  InstanceRole:
    Type: AWS::IAM::Role
    DeletionPolicy: Delete
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service:
                - ec2.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
        - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess
Code language: JSON / JSON with Comments (json)

AWS管理ポリシーAmazonEC2ContainerRegistryFullAccessをアタッチして、ECRリポジトリへのフルアクセスを許可します。
ちなみに本ポリシーの内容は以下の通りです。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ecr:*",
                "cloudtrail:LookupEvents"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "iam:CreateServiceLinkedRole"
            ],
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "iam:AWSServiceName": [
                        "replication.ecr.amazonaws.com"
                    ]
                }
            }
        }
    ]
}
Code language: JSON / JSON with Comments (json)

ECR

Resources:
  ECRRepository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: !Ref Prefix
      RepositoryPolicyText:
        Version: 2012-10-17
        Statement:
          - Effect: Deny
            Principal: "*"
            Action:
              - ecr:BatchCheckLayerAvailability
              - ecr:CompleteLayerUpload
              - ecr:InitiateLayerUpload
              - ecr:PutImage
              - ecr:UploadLayerPart
            Condition:
              ArnLike:
                aws:SourceArn: CodeBuildRoleArn
          - Effect: Deny
            Principal: "*"
            Action:
              - ecr:BatchGetImage
              - ecr:GetDownloadUrlForLayer
            Condition:
              ArnLike:
                aws:SourceArn: FargateTaskExecutionRoleArn
          - Effect: Deny
            Principal: "*"
            NotAction:
              - ecr:BatchDeleteImage
              - ecr:ListImages
            Condition:
              ArnEquals:
                aws:PrincipalArn: !GetAtt ECRFunctionRole.Arn
          - Effect: Deny
            Principal: "*"
            Action: "*"
            Condition:
              ArnNotEquals:
                aws:PrincipalArn:
                  - !Ref CodeBuildRoleArn
                  - !Ref FargateTaskExecutionRoleArn
                  - !GetAtt ECRFunctionRole.Arn
Code language: YAML (yaml)

RepositoryPolicyTextプロパティでリポジトリポリシーを設定します。

リポジトリポリシーの1, 2, 4番目のステートメントがポイントです。

1番目のステートメントはCodeBuild用です。
Condition要素でArnEqualsおよびaws:PrincipalArnを使用して、CodeBuildにアタッチしたIAMロールのARNを指定します。
Effect要素に「Deny」を指定した上で、NotAction要素を使用することによって、NotActionで指定されたアクション以外を拒否することになります。
NotActionにはイメージプッシュに必要なアクションを指定しましたので、CodeBuildはプッシュ以外のアクションが拒否されます。

2番目のステートメントはECSタスク実行用です。
1つ目と同様の記法で、ECSタスク実行にイメージプル以外のアクションを拒否します。

3番目のステートメントはLambda関数用です。
ECRリポジトリ内のイメージを削除するためのCloudFormationカスタムリソース用の関数ですから、イメージを削除する以外のアクションを拒否します。

4番目のステートメントは上記以外のリソースからアクセス用です。
Condition要素でArnNotEqualsおよびaws:PrincipalArnを使用して、上述のIAMロールのARNを指定し、これら以外のリソースを対象とします。
Effect要素に「Deny」を、Actionに「*」を指定することによって、全てのアクションを拒否することになります。
つまり3つのリソース以外のアクセスの場合は、全て拒否されるということです。
ですからEC2インスタンスからのアクセスは全て拒否されます。

(参照)アプリコンテナ

Dockerfile

FROM amazonlinux

RUN yum update -y && yum install python3 python3-pip -y

RUN pip3 install bottle

COPY main.py ./

CMD ["python3", "main.py"]

EXPOSE 8080
Code language: Dockerfile (dockerfile)

アプリコンテナ用のイメージは、Amazon Linux 2をベースとして作成します。

Python製WebフレームワークのBottleを使用します。
ですからPythonおよびpipをインストール後、これをインストールします。

アプリロジックを記載したPythonスクリプト(main.py)をコピーし、これを実行するように設定します。

先述の通り、アプリは8080/tcpでHTTPリクエストを待ち受けますので、このポートを公開します。

main.py

from bottle import route, run

@route('/')
def hello():
  return 'Hello CodePipeline.'

if __name__ == '__main__':
  run(host='0.0.0.0', port=8080)
Code language: Python (python)

Bottleを使用して、簡易的なWebサーバを構築します。
8080/tcpでHTTPリクエストを待ち受け、「Hello CodePipeline.」を返すという単純な構成です。

環境構築

CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。

CloudFormationスタックを作成し、スタック内のリソースを確認する

CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

あわせて読みたい
CloudFormationのネストされたスタックで環境を構築する 【CloudFormationのネストされたスタックで環境を構築する方法】 CloudFormationにおけるネストされたスタックを検証します。 CloudFormationでは、スタックをネストす...

各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。

  • ECR:fa-084
  • CodeCommit:fa-084
  • CodeBuild:fa-084
  • CodePipeline:fa-084
  • EC2インスタンス:i-0fc9dd4b8452544b4

作成されたリソースをAWS Management Consoleから確認します。
ECRを確認します。

Detail of ECR 1.

ECRリポジトリが作成されています。

リポジトリポリシーを確認します。

Detail of ECR 2.

CloudFormationテンプレートで定義した通りに、リポジトリポリシーが作成されていることがわかります。

ECS(Fargate)のクラスター・サービスを確認します。

Detail of ECS 1.
Detail of ECS 2.

正常にクラスター・サービスが作成されています。
ポイントは希望数が0となっている点です。
つまりFargateの初期構築時は、1つもタスクが起動していません。

CodePipelineを確認します。

Detail of CodePipeline 1.

パイプラインの実行に失敗しています。
これはCloudFormationスタック作成時に、CodeCommitが作成されたことがきっかけとして、パイプラインが実行されたためです。
現時点ではCodeCommitにコードをプッシュしていないため、パイプライン実行の過程でエラーが発生しました。

作成されているステージに注目します。
Invokeという名前で、Lambda関数を呼び出すステージが、パイプラインの一番最後に用意されています。
つまりCodeCommitによってプッシュされたコードから、Dockerイメージをビルドして、Fargateにイメージをデプロイした後に、Fargateタスクの希望数を変更するという流れになります。

動作確認

準備が整いましたので、CodeCommitにコードをプッシュします。

まずCodeCommitをプルします。

$ git clone https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/fa-084
Cloning into 'fa-084'...
Code language: Bash (bash)

空のリポジトリがプルされました。

リポジトリにDockerfileおよびmain.pyを加えます。

$ ls -al
total 8
drwxrwxr-x 3 ec2-user ec2-user  51 Sep 19 05:29 .
drwxrwxr-x 3 ec2-user ec2-user  20 Sep 19 05:29 ..
-rw-rw-r-- 1 ec2-user ec2-user 187 Aug 12 11:01 Dockerfile
drwxrwxr-x 7 ec2-user ec2-user 119 Sep 19 05:28 .git
-rw-rw-r-- 1 ec2-user ec2-user 681 Sep 19 05:25 main.py
Code language: Bash (bash)

2ファイルをCodeCommitにプッシュします。

$ git add .

$ git commit -m "first commit"
[master (root-commit) 34a0238] first commit
...
 2 files changed, 39 insertions(+)
 create mode 100644 Dockerfile
 create mode 100644 main.py

$ git push
...
 * [new branch]      master -> master
Code language: Bash (bash)

しばらく待機した後、改めてCodePipelineを確認します。

Detail of CodePipeline 2.

パイプラインが正常に完了しました。

Fargateを確認します。

Detail of ECS 3.
Detail of ECS 4.

希望数が1になりました。
Lambda関数が実行されて希望数が変更されたということです。
希望数が1となったため、ECSタスクが自動生成されました。
このタスクの詳細を見ると、割り当てられたプライベートアドレスが「10.0.2.234」であることがわかります。

コンテナにHTTPリクエストを行うために、EC2インスタンスにアクセスします。
インスタンスへのアクセスはSSM Session Managerを使用します。

% aws ssm start-session --target i-0fc9dd4b8452544b4

Starting session with SessionId: root-01ce255b73c1d7489
sh-4.2$
Code language: Bash (bash)

SSM Session Managerの詳細につきましては、以下のページをご確認ください。

あわせて読みたい
LinuxインスタンスにSSM Session Manager経由でアクセスする 【LinuxインスタンスにSSM Session Manager経由でアクセスする】 EC2インスタンスにSSM Session Manager経由でアクセスする構成を確認します。 Session Manager は完全...

curlコマンドを使って、タスク内のコンテナにアクセスします。

sh-4.2$ curl http://10.0.3.70:8080
Hello CodePipeline.
Code language: Bash (bash)

コンテナから応答がありました。
このことからリポジトリポリシーに従って、CodeBuildによるイメージのプッシュと、ECSタスク実行時のイメージのプルが、正常に行われたことがわかります。

手動でイメージプッシュ/プル

EC2インスタンスからイメージのプッシュ/プルを試みて、リポジトリポリシーの動作を確認します。
まずイメージのプッシュです。

sh-4.2$ sudo su --login ec2-user

[ec2-user@ip-10-0-2-214 ~]$ echo "FROM amazonlinux" > Dockerfile

[ec2-user@ip-10-0-2-214 ~]$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin [account-id].dkr.ecr.ap-northeast-1.amazonaws.com
...
Login Succeeded

[ec2-user@ip-10-0-2-214 ~]$ docker build -t fa-084 .
Sending build context to Docker daemon    341kB
Step 1/1 : FROM amazonlinux
latest: Pulling from library/amazonlinux
12df598bc31e: Pull complete
Digest: sha256:cb8a67164376ecca3b9993e6bb7d81dd868b7836d2631582becd140c8edf27bf
Status: Downloaded newer image for amazonlinux:latest
 ---> 06c59c262be8
Successfully built 06c59c262be8
Successfully tagged fa-084:latest

[ec2-user@ip-10-0-2-214 ~]$ docker tag fa-084:latest [account-id].dkr.ecr.ap-northeast-1.amazonaws.com/fa-084:latest

[ec2-user@ip-10-0-2-214 ~]$ docker push [account-id].dkr.ecr.ap-northeast-1.amazonaws.com/fa-084:latest
The push refers to repository [[account-id].dkr.ecr.ap-northeast-1.amazonaws.com/fa-084]
bb99a8750875: Retrying in 1 second
EOF
Code language: Bash (bash)

ECRリポジトリにイメージをプッシュするコマンドを実行した結果、プッシュ時にタイムアウトして失敗しました。

続いてイメージのプルです。

[ec2-user@ip-10-0-2-214 ~]$ docker pull [account-id].dkr.ecr.ap-northeast-1.amazonaws.com/fa-084:latest
Error response from daemon: pull access denied for [account-id].dkr.ecr.ap-northeast-1.amazonaws.com/fa-084, repository does not exist or may require 'docker login': denied: User: arn:aws:sts::[account-id]:assumed-role/fa-084-EC2Stack-1JOEIS9JU31P0-InstanceRole-GI0SW6JN8M66/i-0fc9dd4b8452544b4 is not authorized to perform: ecr:BatchGetImage on resource: arn:aws:ecr:ap-northeast-1:[account-id]:repository/fa-084 with an explicit deny in a resource-based policy
Code language: Bash (bash)

イメージのプルも失敗しました。

EC2インスタンスにアタッチしたIAMロールでは、ECRへのフルアクセスを許可しています。
しかしリポジトリポリシーによって、手動によるイメージのプッシュ/プルのアクションは拒否されたということです。

まとめ

リポジトリポリシーを使用して、CodePipeline内のECRリポジトリへの手動プッシュ/プルを防止する方法を確認しました。