AWS CodeBuild で特権モードを使わずにイメージのビルドを実行する

AWS CodeBuild で特権モードを使わずにイメージのビルドを実行する

Clock Icon2025.01.31

はじめに

データアナリティクス事業本部のkobayashiです。

CodeBuildで特権モードを無効にしてコンテナイメージをビルドする要件があったのですがkanikoを使って解決したのでその内容をまとめます。

kaniko on Codebuid

手法としてはAWSブログの「Amazon ECS on AWS Fargate を利用したコンテナイメージのビルド | Amazon Web Services ブログ 」の記事にヒントがありましたこの記事の中ではFargateでのビルドを行なう際に同じく特権モードを使用しないビルドとしてkanikoが使われていたのでこちらを使用します。
以下記事からの引用です。

ここ数年、特権モードを必要とせずにコンテナイメージをビルドしたいという問題を解決するために新しいツールが登場しています。kaniko はそのようなツールの一つで、従来の Docker と同じように Dockerfile からコンテナイメージをビルドします。しかし Dockerとは異なり、root 権限を必要とせず Dockerfile 内の各コマンドを完全にユーザー名前空間内で実行します。そのため、実行中のコンテナ内で root 権限無しでコンテナイメージをビルドできます。

https://github.com/GoogleContainerTools/kaniko

特権モードなしのビルドをやってみる

ではkanikoを使ってCodeBuildで特権モードを使わない状態でビルドを行ってみます。
最終的な成果物はPythonコンテナで、以下のスクリプトを実行するイメージを作成することです。

main.py
import random

from ulid import ULID

def main():
  population = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  sample_size = 3

  sample = random.sample(population, sample_size)

  print(f"hello world {sample} {ULID()}")

main()
$ python main.py
hello world [8, 2, 9] 01JHVV25982T93QDTV59GQ2Z9E

CodeBuildほかAWSリソースの作成

はじめにCodeBuildプロジェクトとECRリポを作成します。 以下のterraform使ってAWSリソースを作成します。

data "aws_caller_identity" "current" {}
variable "aws_region" { default = "ap-northeast-1" }
variable "project_name" { default = "non-privileged-build" }
variable "env" { default = "develop" }

##### ビルドするイメージ #####
resource "aws_ecr_repository" "target" {
  name                 = "${var.project_name}-${var.env}-target-ecr"
  image_tag_mutability = "MUTABLE"

  force_delete = true
}

##### ビルドファイル #####
resource "aws_s3_bucket" "source" {
  bucket = join("-", [var.project_name, var.env, "source", data.aws_caller_identity.current.account_id])

  force_destroy = true
}
resource "aws_s3_bucket_server_side_encryption_configuration" "source" {
  bucket = aws_s3_bucket.source.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

##### ビルド用イメージ #####
resource "aws_ecr_repository" "builder" {
  name                 = "${var.project_name}-${var.env}-builder-ecr"
  image_tag_mutability = "MUTABLE"

  force_delete = true
}

resource "aws_ecr_repository_policy" "builder" {
  repository = aws_ecr_repository.builder.name

  policy = data.aws_iam_policy_document.builder.json
}

data "aws_iam_policy_document" "builder" {
  statement {
    effect = "Allow"

    principals {
      type = "Service"

      identifiers = ["codebuild.amazonaws.com"]
    }

    # Delete以外を与える
    actions = [
      "ecr:BatchCheckLayerAvailability",
      "ecr:BatchGetImage",
      "ecr:GetDownloadUrlForLayer"
    ]
  }
}

##### codebuild #####
resource "aws_codebuild_project" "main" {

  name                   = "${var.project_name}-${var.env}-builder"
  build_timeout          = "10"
  concurrent_build_limit = 20
  service_role           = aws_iam_role.cb.arn

  source {
    type     = "S3"
    location = "${aws_s3_bucket.source.bucket}/docker.zip"
  }

  environment {
    compute_type                = "BUILD_GENERAL1_SMALL"
    image                       = "${aws_ecr_repository.builder.repository_url}:latest"
    type                        = "LINUX_CONTAINER"
    image_pull_credentials_type = "SERVICE_ROLE"
    privileged_mode             = false # <<<<<<<< 特権モードを使わない

    environment_variable {
      name  = "target_repo_uri"
      value = aws_ecr_repository.target.repository_url
    }
  }

  logs_config {
    cloudwatch_logs {
      group_name  = aws_cloudwatch_log_group.main.name
      stream_name = var.env
    }
  }

  artifacts {
    type = "NO_ARTIFACTS"
  }
}

# CloudWatch Logs
resource "aws_cloudwatch_log_group" "main" {
  name = "/${var.project_name}-${var.env}/app_codebuild"
}

#####  IAM #####

# codebuild
resource "aws_iam_role" "cb" {
  name               = "${var.project_name}-${var.env}-codebuild-role"
  path               = "/service-role/"
  assume_role_policy = <<POLICY
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "codebuild.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
POLICY
}

data "aws_iam_policy_document" "cb" {
  statement {
    actions = ["ecr:GetAuthorizationToken"]
    resources = ["*"]
  }
  statement {
    actions = [
      "ecr:BatchCheckLayerAvailability",
      "ecr:GetDownloadUrlForLayer",
      "ecr:BatchGetImage",
      "ecr:ListTagsForResource"
    ]
    resources = [aws_ecr_repository.builder.arn]
  }
  statement {
    actions = [
      "ecr:BatchCheckLayerAvailability",
      "ecr:GetDownloadUrlForLayer",
      "ecr:BatchGetImage",
      "ecr:ListTagsForResource",
      "ecr:PutImage",
      "ecr:CompleteLayerUpload",
      "ecr:UploadLayerPart",
      "ecr:InitiateLayerUpload",
    ]
    resources = [aws_ecr_repository.target.arn]
  }
  statement {
    actions = [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]
    resources = [
      aws_cloudwatch_log_group.main.arn,
      "${aws_cloudwatch_log_group.main.arn}:*"
    ]
  }
  statement {
    actions = ["s3:ListBucket"]
    resources = ["arn:aws:s3:::${aws_s3_bucket.source.bucket}"]
  }
  statement {
    actions = [
      "s3:GetObject",
      "s3:DeleteObject",
      "s3:PutObject"
    ]
    resources = ["arn:aws:s3:::${aws_s3_bucket.source.bucket}/*"]
  }
}

resource "aws_iam_policy" "cb" {
  name        = "${var.project_name}-${var.env}-codebuild-policy"
  path        = "/"
  description = "${var.project_name}-${var.env} codebuild Policy"

  policy = data.aws_iam_policy_document.cb.json
}

resource "aws_iam_role_policy_attachment" "cb" {
  role       = aws_iam_role.cb.id
  policy_arn = aws_iam_policy.cb.arn
}

privileged_mode = falseでCodeBuildの特権モードを使わない設定にしてあります。

作成した代表的なリソースは次のようになります。

  • ECRリポジトリnon-privileged-build-develop-target-ecr
    • 作成したPythonスクリプトを実行するコンテナイメージ用ECRリポジトリ
  • CodeBuildプロジェクトnon-privileged-build-develop-builder
    • コンテナイメージビルド用プロジェクト
  • ECRリポジトリnon-privileged-build-develop-builder-ecr
    • CodeBuildで使うECRリポジトリ
    • kanikoでイメージをビルドするカスタムイメージ
  • S3バケットnon-privileged-build-develop-source-0123456789
    • CodeBuildのソースプロバイダ
    • Pythonのスクリプト、Dockerfileの置き場

CodeBuild のカスタム Docker イメージの作成

次にCodeBuild のカスタム Docker イメージを作成してCodeBuildで使うECRリポジトリnon-privileged-build-develop-builder-ecrにpushします。ここが本記事のポイントになります。

FROM gcr.io/kaniko-project/executor:v1.23.2-debug as kaniko

FROM alpine

RUN apk update && \
  apk upgrade && \
  apk add --no-cache aws-cli git curl jq

COPY --from=kaniko /kaniko /kaniko

ENV PATH=/kaniko:$PATH
ENV SSL_CERT_DIR=/kaniko/ssl/certs
ENV DOCKER_CONFIG=/kaniko/.docker/
ENV DOCKER_CREDENTIAL_GCR_CONFIG=/kaniko/.config/gcloud/docker_credential_gcr_config.json

ENTRYPOINT ["/kaniko/executor"]

ベースはaplineのイメージを使っています。

  1. はじめにCodeBuildで必要なaws-cliなど必要なライブラリをインストールします。
  2. 次にkaniko最新版のdebugイメージからkanikoの実行ファイルとその関連ファイルをコピーします。
  3. その上で環境変数としてPATHにkanikoディレクトリを追加し、SSL証明書ディレクトリ、Dockerのconfigディレクトリを指定しています。
  4. 最後にコンテナ起動時にkanikoのexecutorを実行してビルドを行なうようにします。

このイメージをビルドしてCodeBuildで使うECRリポジトリnon-privileged-build-develop-builder-ecrにpushします

$ aws-vault exec cm_sspg -- aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 0123456789.dkr.ecr.ap-northeast-1.amazonaws.com
$ docker build . -t 0123456789.dkr.ecr.ap-northeast-1.amazonaws.com/non-privileged-build-develop-builder-ecr:latest
$ docker push 0123456789.dkr.ecr.ap-northeast-1.amazonaws.com/non-privileged-build-develop-builder-ecr:latest

非特権モードでのCodeBuild実行

これでCodeBuildで非特権モードでコンテナイメージをビルドする準備ができたので冒頭のPythonスクリプトを実行するコンテナイメージを作成してみます。
はじめにPythonスクリプトを含む以下のファイル群を作成します。

.
├── Dockerfile
├── buildspec.yml
├── main.py
└── requirements.txt

main.pyは冒頭のファイルになります。他のファイルは以下のようにしました。

requirements.txt
python-ulid 

main.pyでulidのパッケージを使っているので追加してあります。

Dockerfile
FROM python:3.12

RUN pip install --upgrade pip
RUN pip install --upgrade setuptools

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY main.py ./

ENTRYPOINT ["python","main.py"]

python3.12のベースイメージにrequirements.txtでパッケージをインストールしてコンテナ起動時にpython main.pyを実行するだけの簡単な構成です。

buildspec.yml
version: 0.2

env:
  git-credential-helper: yes
phases:
  pre_build:
    commands:
      - echo Set AWS Credential...
      - AWS_CREDENTIAL=$(curl -s http://169.254.170.2${AWS_CONTAINER_CREDENTIALS_RELATIVE_URI})
      - AWS_ACCESS_KEY_ID=`echo ${AWS_CREDENTIAL} | jq -r .AccessKeyId`
      - AWS_SECRET_ACCESS_KEY=`echo ${AWS_CREDENTIAL} | jq -r .SecretAccessKey`
      - AWS_SESSION_TOKEN=`echo ${AWS_CREDENTIAL} | jq -r .Token`
  build:
    commands:
      - echo Building the Container image...
      - pwd
      - /kaniko/executor --force --context $(pwd) --dockerfile $(pwd)/Dockerfile -d ${target_repo_uri} --build-arg AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID --build-arg AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY --build-arg AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN
  post_build:
    commands:
      - echo Pushing the Container images...

CodeBuildで使用するbuildspec.ymlです。ここではkanikoを実行することでコンテナイメージをビルドし、作成したイメージをECRリポジトリnon-privileged-build-develop-target-ecrにpushしています。
そのため、pre_buildで認証情報を取得し、buildブロックで/kaniko/executorを実行するときに--build-argとして認証情報を渡すことでECRリポジトリにpushまでkaniko/executorで行っています。
他のkanikoのコマンドのオプションは以下に記載がありますのでご確認ください。

これらのファイル群をdocker.zipに固めてS3にアップロードします。
あとはビルドを実行するだけなので以下のコマンドで実行します。

$ aws codebuild start-build --project-name non-privileged-build-develop-builder
ビルドログ
[Container] 2025/01/18 05:20:56.367829 Running on CodeBuild On-demand
[Container] 2025/01/18 05:20:56.367945 Waiting for agent ping
[Container] 2025/01/18 05:20:57.573019 Waiting for DOWNLOAD_SOURCE
[Container] 2025/01/18 05:20:57.678177 Phase is DOWNLOAD_SOURCE
[Container] 2025/01/18 05:20:57.720573 CODEBUILD_SRC_DIR=/codebuild/output/src2084498944/src
[Container] 2025/01/18 05:20:57.721043 YAML location is /codebuild/output/src2084498944/src/buildspec.yml
[Container] 2025/01/18 05:20:57.722897 Setting HTTP client timeout to higher timeout for S3 source
[Container] 2025/01/18 05:20:57.722986 Processing environment variables
...(略)
[Container] 2025/01/18 05:20:58.035406 Entering phase BUILD
[Container] 2025/01/18 05:20:58.036505 Running command echo Building the Container image...
Building the Container image...
[Container] 2025/01/18 05:20:58.044077 Running command pwd
/codebuild/output/src2084498944/src
[Container] 2025/01/18 05:20:58.047829 Running command /kaniko/executor --force --context $(pwd) --dockerfile $(pwd)/Dockerfile -d ${target_repo_uri} --build-arg AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID --build-arg AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY --build-arg AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN
INFO[0000] Retrieving image manifest python:3.12        
INFO[0000] Retrieving image python:3.12 from registry index.docker.io 
INFO[0001] Built cross stage deps: map[]                
INFO[0001] Retrieving image manifest python:3.12        
INFO[0001] Returning cached image manifest              
INFO[0001] Executing 0 build triggers                   
INFO[0001] Building stage 'python:3.12' [idx: '0', base-idx: '-1'] 
INFO[0001] Unpacking rootfs as cmd RUN pip install --upgrade pip requires it. 
INFO[0016] RUN pip install --upgrade pip                
INFO[0016] Initializing snapshotter ...                 
INFO[0016] Taking snapshot of full filesystem...        
INFO[0033] Cmd: /bin/sh                                 
INFO[0033] Args: [-c pip install --upgrade pip]         
INFO[0033] Running: [/bin/sh -c pip install --upgrade pip] 
Requirement already satisfied: pip in /usr/local/lib/python3.12/site-packages (24.3.1)
INFO[0035] Taking snapshot of full filesystem...        
INFO[0039] RUN pip install --upgrade setuptools         
INFO[0039] Cmd: /bin/sh                                 
INFO[0039] Args: [-c pip install --upgrade setuptools]  
INFO[0039] Running: [/bin/sh -c pip install --upgrade setuptools] 
Collecting setuptools
  Downloading setuptools-75.8.0-py3-none-any.whl.metadata (6.7 kB)
Downloading setuptools-75.8.0-py3-none-any.whl (1.2 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.2/1.2 MB 56.4 MB/s eta 0:00:00
Installing collected packages: setuptools
Successfully installed setuptools-75.8.0
INFO[0040] Taking snapshot of full filesystem...        
INFO[0042] COPY requirements.txt ./                     
INFO[0042] Taking snapshot of files...                  
INFO[0042] RUN pip install --no-cache-dir -r requirements.txt 
INFO[0042] Cmd: /bin/sh                                 
INFO[0042] Args: [-c pip install --no-cache-dir -r requirements.txt] 
INFO[0042] Running: [/bin/sh -c pip install --no-cache-dir -r requirements.txt] 
Collecting python-ulid (from -r requirements.txt (line 1))
  Downloading python_ulid-3.0.0-py3-none-any.whl.metadata (5.8 kB)
Downloading python_ulid-3.0.0-py3-none-any.whl (11 kB)
Installing collected packages: python-ulid
Successfully installed python-ulid-3.0.0
INFO[0043] Taking snapshot of full filesystem...        
INFO[0045] COPY main.py ./                              
INFO[0045] Taking snapshot of files...                  
INFO[0045] ENTRYPOINT ["python","main.py"]              
INFO[0045] Pushing image to 0123456789.dkr.ecr.ap-northeast-1.amazonaws.com/non-privileged-build-develop-target-ecr 
INFO[0046] Pushed 0123456789.dkr.ecr.ap-northeast-1.amazonaws.com/non-privileged-build-develop-target-ecr@sha256 
[Container] 2025/01/18 05:21:44.656881 Phase complete: BUILD State: SUCCEEDED
[Container] 2025/01/18 05:21:44.656906 Phase context status code:  Message: 
[Container] 2025/01/18 05:21:44.702550 Entering phase POST_BUILD
[Container] 2025/01/18 05:21:44.703367 Running command echo Pushing the Container images...
Pushing the Container images...
[Container] 2025/01/18 05:21:44.709143 Phase complete: POST_BUILD State: SUCCEEDED
[Container] 2025/01/18 05:21:44.709156 Phase context status code:  Message: 
[Container] 2025/01/18 05:21:44.762952 Set report auto-discover timeout to 5 seconds
[Container] 2025/01/18 05:21:44.765516 Expanding base directory path:  .
[Container] 2025/01/18 05:21:44.766891 Assembling file list
[Container] 2025/01/18 05:21:44.766904 Expanding .
[Container] 2025/01/18 05:21:44.769367 Expanding file paths for base directory .
[Container] 2025/01/18 05:21:44.769378 Assembling file list
[Container] 2025/01/18 05:21:44.769381 Expanding **/*
[Container] 2025/01/18 05:21:44.773017 No matching auto-discover report paths found
[Container] 2025/01/18 05:21:44.773080 Report auto-discover file discovery took 0.010128 seconds
[Container] 2025/01/18 05:21:44.773094 Phase complete: UPLOAD_ARTIFACTS State: SUCCEEDED
[Container] 2025/01/18 05:21:44.773102 Phase context status code:  Message: 

このように特権モードがなくても無事にビルドが行えました。

まとめ

CodeBuildで特権モードを無効にしてコンテナイメージをビルドするためにkanikoを使ってカスタムイメージを作成してCodebuildで使ってみました。セキュリティ要件でCodeBuildの特権モードを使えない場合にはこの方法を使うことでビルドができるのでお試しください。

最後まで読んで頂いてありがとうございました。

参考

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.