AWS CodeBuild で特権モードを使わずにイメージのビルドを実行する
はじめに
データアナリティクス事業本部のkobayashiです。
CodeBuildで特権モードを無効にしてコンテナイメージをビルドする要件があったのですがkanikoを使って解決したのでその内容をまとめます。
kaniko on Codebuid
手法としてはAWSブログの「Amazon ECS on AWS Fargate を利用したコンテナイメージのビルド | Amazon Web Services ブログ 」の記事にヒントがありましたこの記事の中ではFargateでのビルドを行なう際に同じく特権モードを使用しないビルドとしてkanikoが使われていたのでこちらを使用します。
以下記事からの引用です。
ここ数年、特権モードを必要とせずにコンテナイメージをビルドしたいという問題を解決するために新しいツールが登場しています。kaniko はそのようなツールの一つで、従来の Docker と同じように Dockerfile からコンテナイメージをビルドします。しかし Dockerとは異なり、root 権限を必要とせず Dockerfile 内の各コマンドを完全にユーザー名前空間内で実行します。そのため、実行中のコンテナ内で root 権限無しでコンテナイメージをビルドできます。
特権モードなしのビルドをやってみる
ではkanikoを使ってCodeBuildで特権モードを使わない状態でビルドを行ってみます。
最終的な成果物はPythonコンテナで、以下のスクリプトを実行するイメージを作成することです。
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 /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のイメージを使っています。
- はじめにCodeBuildで必要なaws-cliなど必要なライブラリをインストールします。
- 次にkaniko最新版のdebugイメージからkanikoの実行ファイルとその関連ファイルをコピーします。
- その上で環境変数としてPATHにkanikoディレクトリを追加し、SSL証明書ディレクトリ、Dockerのconfigディレクトリを指定しています。
- 最後にコンテナ起動時に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は冒頭のファイルになります。他のファイルは以下のようにしました。
python-ulid
main.pyでulidのパッケージを使っているので追加してあります。
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
を実行するだけの簡単な構成です。
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の特権モードを使えない場合にはこの方法を使うことでビルドができるのでお試しください。
最後まで読んで頂いてありがとうございました。