🕵️

CodeBuildのローカルビルドで渡す環境変数ファイルを、ホスト側で展開してからビルドコンテナに渡す

2024/02/16に公開

TL;DR

  • codebuild_build.shでevalで行ごとに評価した後、base64エンコードして変数としてagentに渡す
  • local_build.shでbase64デコードした環境変数をファイルに書き出してcustomer-specific.ymlへ追記する

修正箇所

  • codebuild_build.shの修正
codebuild_build.sh
@@ -46,6 +46,7 @@ function usage {
     echo "  -m        Used to mount the source directory to the customer build container directly."
     echo "  -d        Used to run the build container in docker privileged mode."
     echo "  -e FILE   Used to specify a file containing environment variables."
+    echo "  -x FILE   Used to specify a file containing environment variables and expansion."
     echo "            (-e) File format expectations:"
     echo "               * Each line is in VAR=VAL format"
     echo "               * Lines beginning with # are processed as comments and ignored"
@@ -61,7 +62,7 @@ awsconfig_flag=false
 mount_src_dir_flag=false
 docker_privileged_mode_flag=false
 
-while getopts "cmdi:a:r:s:b:e:l:p:h" opt; do
+while getopts "cmdi:a:r:s:b:e:x:l:p:h" opt; do
     case $opt in
         i  ) image_flag=true; image_name=$OPTARG;;
         a  ) artifact_flag=true; artifact_dir=$OPTARG;;
@@ -72,6 +73,7 @@ while getopts "cmdi:a:r:s:b:e:l:p:h" opt; do
         d  ) docker_privileged_mode_flag=true;;
         s  ) source_dirs+=("$OPTARG");;
         e  ) environment_variable_file=$OPTARG;;
+        x  ) expand_environment_variable_file=$OPTARG;;
         l  ) local_agent_image=$OPTARG;;
         p  ) aws_profile=$OPTARG;;
         h  ) usage; exit;;
@@ -181,6 +183,17 @@ then
 else
     docker_command+=" -e \"INITIATOR=$USER\""
 fi
+ 
+if [ -n "$expand_environment_variable_file" ]
+then
+    customer_envs=""
+    while IFS= read -r line; do
+        eval ${line}
+        eval env=${line}
+        customer_envs+="$(echo $(echo -n "${env}" | base64)) "
+    done < "$expand_environment_variable_file"
+    docker_command+=" -e CUSTOMER_ENVIRONMENTS=\"${customer_envs}\""
+fi
 
  • local_build.shの修正
local_build.sh
@@ -100,6 +100,15 @@ else
         /LocalBuild/agent-resources/bin/edit-docker-compose /LocalBuild/envFile/$ENV_VAR_FILE /LocalBuild/agent-resources/customer-specific.yml "EnvironmentVariables"
     fi
 
+    if [ ! -z "${CUSTOMER_ENVIRONMENTS}" ]
+    then
+        customer_envfile=customer_envs.txt
+        for encoded in $(echo "${CUSTOMER_ENVIRONMENTS}"); do
+            echo "$(echo -n "$encoded" | base64 -d)" >> "${customer_envfile}"
+        done
+        /LocalBuild/agent-resources/bin/edit-docker-compose "${customer_envfile}" /LocalBuild/agent-resources/customer-specific.yml "EnvironmentVariables"
+    fi
+
     # Validate docker-compose config
     docker-compose -f customer-specific.yml config --quiet || exit 1
  • 修正したlocal_build.shを用いてイメージ化
    ※下記記事の続きとしてmy-local-builds:latestを指定
Dockerfile
FROM my-local-builds:latest

COPY "./local_build.sh" "/usr/local/bin/local_build.sh"

実行方法

codebuild_build.sh実行時コマンドの-e 環境変数ファイル-x 環境変数ファイルに置き換える。

command
./codebuild_build.sh \
  -i public.ecr.aws/codebuild/amazonlinux2-x86_64-standard:5.0-24.02.08 \
  -s ./src \
  -a ./artifacts \
  -x .env

背景

codebuild_build.shで渡す環境変数ファイルは変数を含んでいるとコンテナ起動時にエラーになる。
例えばホスト側のカレントディレクトリのコマンド結果を変数に入れて渡そうとする。

.env
HOST_PWD=$(pwd)
command
./codebuild_build.sh \
  -i public.ecr.aws/codebuild/amazonlinux2-x86_64-standard:5.0-24.02.08 \
  -s ./src \
  -a ./artifacts \
  -e .env
ERROR: Invalid interpolation format for "environment" option in service "agent": "CUSTOMER_HOST_PWD=$(pwd)

agent側で変数名にCUSTOMER_を付けられているが、値は展開されないうえenvironmentに渡されるときに解釈不能な文字列としてエラーとなっているようである。

対策方針

もっとシンプルで安全なやり方は色々あると思われるが、あまり深い仕組みは理解できていないので単純に環境変数として渡すこととしている。

空白やエスケープシーケンスが含まれると厄介なので、evalで変数評価後にkey=valueを文字列としてみなし、base64エンコードしてただの環境変数として外側のagentに渡したうえで、base64デコードした結果をファイルに書き出して通常の-eオプションと同様の処理を実行しビルドコンテナ内環境変数として渡している。

※evalの動作として同一シェル内で既存の環境変数が上書きされるだけでなく、そもそも自由にコマンドが実行できるコマンドなので破壊的な動作をさせることも可能なので要注意である。
例えばPATHをファイル内に書いてしまうと、内容によっては後続処理で一切コマンドが実行できなくさせることもできる。

処理詳細
codebuild_build.sh
if [ -n "$expand_environment_variable_file" ]
then
    customer_envs=""
    while IFS= read -r line; do
        eval ${line}
        eval env=${line}
        customer_envs+="$(echo $(echo -n "${env}" | base64)) "
    done < "$expand_environment_variable_file"
    docker_command+=" -e CUSTOMER_ENVIRONMENTS=\"${customer_envs}\""
fi

引数-xが与えられた場合、そのファイルを先頭から一行ずつ読み込み、後続の行でも値を維持させるためにevalで一旦式を評価させている。

さらにその文字列を一時変数envに格納し、base64エンコードして空白結合で一つの変数に格納する。

空初結合した変数をdocker起動コマンドの引数に環境変数CUSTOMER_ENVIRONMENTSとして渡す。

local_build.sh
if [ ! -z "${CUSTOMER_ENVIRONMENTS}" ]
then
    customer_envfile=customer_envs.txt
    for encoded in $(echo "${CUSTOMER_ENVIRONMENTS}"); do
        echo "$(echo -n "$encoded" | base64 -d)" >> "${customer_envfile}"
    done
    /LocalBuild/agent-resources/bin/edit-docker-compose "${customer_envfile}" /LocalBuild/agent-resources/customer-specific.yml "EnvironmentVariables"
fi

環境変数CUSTOMER_ENVIRONMENTSが空文字でない場合、空白を区切り文字として1行ずつ読み込みbase64デコードした結果を一時ファイルに追記していく。

最後に/LocalBuild/agent-resources/bin/edit-docker-composeコマンドというもので、環境変数ファイルを指定してymlファイルのenvironmentsオプションに追記させている。
このコマンドの詳細は全く分からないが、通常の-eオプションで指定されるときに実行される処理を模倣してファイル名を変えてコマンド実行させているだけである。

if [ ! -z "${ENV_VAR_FILE}" ]
then
    /LocalBuild/agent-resources/bin/edit-docker-compose /LocalBuild/envFile/$ENV_VAR_FILE /LocalBuild/agent-resources/customer-specific.yml "EnvironmentVariables"
fi

変数値に空白が入っていてもビルドコンテナの環境変数として渡すことは一応可能だが、すべての動作確認をしているわけではないので保証はできない。

.env
HOST_PWD=$(pwd)
TEST_ENV1="a "
TEST_ENV2="${TEST_ENV1}b"
buildspec.yml
Version: 0.2

phases:
  build:
    commands:
      - echo ${HOST_PWD}
      - "docker run \
        --rm \
        -v ${HOST_PWD}:/root/test \
        ubuntu:22.04 \
        bash -c \
        \"ls -al /root/test && \
        cat \\\"/root/test/${TEST_ENV2}\\\"\""
agent_1  | [Container] ****/**/** **:**:** Running command echo ${HOST_PWD}
agent_1  | /root/local_builds
agent_1  | 
agent_1  | [Container] ****/**/** **:**:** Running command docker run --rm -v ${HOST_PWD}:/root/test ubuntu bash -c "ls -al /root/test && cat \"/root/test/${TEST_ENV2}\""
agent_1  | total *
agent_1  | drwxr-xr-x * root root    4096 *** ** **:** .
agent_1  | drwx------ * root root    4096 *** ** **:** ..
agent_1  | -rw-r--r-- 1 root root       * *** ** **:** .env
agent_1  | content of "a b"
agent_1  | 
agent_1  | [Container] ****/**/** **:**:** Phase complete: BUILD State: SUCCEEDED

続き

Discussion