りんごとバナナとエンジニア

エンジニア修行の記録

sh -cでコマンドを渡すときはシングルクォートを使う

KubernetesのPod内の環境変数が正しく設定されているかを確認しようとして、以下のようにしたところ、何も出力されなかった。

$ kubectl exec -it pod_name -- /bin/sh -c "echo $VAR_NAME"

Podの中に入って確認すると正しく出る。

$ kubectl exec -it pod_name -- /bin/sh
# echo $VAR_NAME
test

これは、 /bin/sh -c に渡したコマンドがダブルクォートだったことが原因。 sh -c を使うと、 -c 以下で渡したコマンドを sh で実行させることができるが、ダブルクォートにしてしまうと kubectl を実行しているシェルで変数が解釈されてしまう。 $VAR_NAME という環境変数はPodの中にはあるがhostでは設定されていないので、単なる echo を渡していることと同じになり、何も出力されずに終わる。

正しく確認するには、シングルクォートで渡せば良い。シングルクォートで囲われた内容は展開されないため、 $VAR_NAME がPodに渡る。

$ kubectl exec -it pod_name -- /bin/sh -c 'echo $VAR_NAME'
test

netcatのzero I/Oモードで疎通確認する

疎通確認をする際はcurlwgetを使うことが多かったが、CKADの講座で講師がnetcatを使っているのを見て調べてみた。

netcatはサービスとの通信を確認できるコマンドであり、たいていのLinuxディストリビューションに同梱されている。KubernetesのPodに入ってコマンドを打つ場合、imageにcurlが入っていないというようなことも多いので、使えるようにしておくと便利かもしれない。

netcatでの通信は、基本的に相手のホスト名とポートを指定すればよい。

nc google.com 80

これに加え、netcatではzero I/Oモードを使ったポートスキャンを行うことができる。これはパケットを飛ばさず、相手のポートに接続できるかのみを確認するモードで、接続に成功すればすぐに接続が閉じられる。実際にデータのリクエスト・レスポンスをすることがないので、疎通確認に適している。

CKAD講座では、Podの中からService名を指定して疎通できるか確認していた。

nc -z -v -w 1 service-name 80

-z でzero I/Oモードを使える。 -v は詳細な出力をするオプション。 -wタイムアウト判定までの秒数を指定するもので、これを指定しないと接続に失敗した場合コマンドが終了しない。

【GitHub Actions】AWS Lambdaの環境変数を自動で更新する

最近システムのリリースフローの自動化を進めることが多い。先日はLambda関数の更新の自動化を行った。この関数は環境変数を持っており、これを頻繁に更新する必要があった。

新しい環境変数をどのように渡すかが最も難しいポイントだったが、この環境変数AWS CLIコマンドにおいてJSONファイル形式で渡すことができる。

docs.aws.amazon.com

aws lambda update-function-configuration --function-name "${function_name}" --environment fileb://env.json

--environment オプションの後にファイルパスを指定する。 AWS CLIでは fileb:// とつけることで、ファイルの中身をバイナリコンテンツとして渡すことができる。面倒なファイル読み込みの処理を書かずに済むため、この記法はとても気に入っている。また、このJSONファイルをGitHub上に置けるため、環境変数のバージョン管理がしやすくなるという利点もある。

JSONファイルのフォーマットは以下の通り。 SECRET_ENV は機密性の高い環境変数であり、値はGitHub ActionsのSecretsに登録してある。JSONファイル上では値を適当なものにしておく。

# env.json
{
  "Variables": {
    "NORMAL_ENV": "example",
    "SECRET_ENV": "to_be_converted"
  }
}

workflowファイルには以下のように記載する。 jq コマンドでJSONファイルの SECRET_ENV の値を置換しているのがポイント。 --arg オプションとして secret_env として ${{ secrets.SECRET_ENV }} の値を渡し、その値をJSONファイル内の to_be_converted と入れ替えている。

- name: Update lambda envvar
      env:
        LAMBDA_FUNCTION_NAME: dev_function
        ENV_JSON_FILE: env.json
        SECRET_ENV: ${{ secrets.SECRET_ENV }}
      run: |
        jq --arg secret_env "${SECRET_ENV}" '.Variables.SECRET_ENV = $secret_env' "${ENV_JSON_FILE}" > env_converted.json
        aws lambda update-function-configuration --function-name "${LAMBDA_FUNCTION_NAME}" --environment fileb://env_converted.json

【Kubernetes】--dry-runでmanifestファイルの雛形を手に入れる

CKAD対策で学んだtips。
Kubernetes上のリソースを作る際、複雑な設定の場合はmanifestファイルを書かなければいけない。しかしYAML形式で記述を行うのはとても時間がかかるうえ、それぞれのリソース種別ごとの記法を全部は覚えられない。公式ドキュメントからコピペしてくることもできるが、それも探すのが面倒なうえタイムロスになる。
そこで、manifestファイルの雛形を手に入れたいときは --dry-run が役立つ。

--dry-run はkubectlコマンドのオプションで、 applycreate を実行せず、クライアントサイドでできるバリデーションを行ってくれる。この出力をYAML形式でファイルに書き出せば、それがmanifestファイルの雛形になる。

$ kubectl run nginx --image=nginx --dry-run=client -o yaml > nginx.yaml
$ cat nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    run: nginx
  name: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      run: nginx
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        run: nginx
    spec:
      containers:
      - image: nginx
        name: nginx
        resources: {}
status: {}

これをもとに各項目の値を変えることができる。ゼロからmanifestファイルを作るよりはるかに楽。
なお、設定すべき項目のうちコマンド上で設定できるオプションをあらかじめ設定してファイルに出力すると、作業がより効率的になる。

【GitHub Actions】ファイルパスのワイルドカード指定には「*」では不十分

先日いつものようにプルリクエストを作ったら、GitHub Actionsのworkflowが全く動いてくれなかった。GitHub ActionsのSpending limitやGitHub自体の障害も疑ったが、原因はもっと単純なものだった。

yamlファイルでは、 on 節を以下のように記述していた。

on:
  pull_request:
    branches:
      - master
    paths:
      - '*'
      - '!README.md'
      - '!.gitignore'
      - '!.github/workflows/dev-workflow.yaml'
      - '!.github/workflows/release-workflow.yaml'
...

これでコードのファイルは全て検知すると思っていたが、 * を指定していたのが問題だった。GitHub Actionsでは、 * ではスラッシュにマッチしないため、これではトップディレクトリにあるファイルしか検知してくれない。

docs.github.com

workflowが動かなかったプルリクエストでは、 src/main.py のみに差分があったため、上記の条件では検知されなかった。それ以前のプルリクエストでは、READMEやworkflowファイルを合わせて更新していたので、たまたま問題なかっただけだった。

サブディレクトリ以下にあるファイルにもマッチさせたい場合、 ** と指定すればよい。

on:
  pull_request:
    branches:
      - master
    paths:
      - '**'
      - '!README.md'
      - '!.gitignore'
      - '!.github/workflows/dev-workflow.yaml'
      - '!.github/workflows/release-workflow.yaml'
...

ただ、このように「少数のファイル以外全てにマッチさせたい」という場合、 paths-ignore を使う方がより単純に書ける。

docs.github.com

on:
  pull_request:
    branches:
      - master
    paths-ignore:
      - 'README.md'
      - '.gitignore'
      - '.github/workflows/dev-workflow.yaml'
      - '.github/workflows/release-workflow.yaml'
...

【GitHub Actions】docker-build-pushでECRにimageをpushする

GitHub Actionsでworkflowを書くとき、imageをビルドしてECRにpushする処理を書く機会が多い。今までは以下の様に書いていた。

jobs:
  build:
    name: Build and push Docker image
    runs-on: ubuntu-latest
    env:
      IMAGE_NAME: ${{ secrets.CONTAINER_REGISTRY_PATH }}/path/to/ecr/repo
      IMAGE_TAG: ${GITHUB_SHA}
    steps:
    - uses: actions/checkout@master
    - name: Login to ECR
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      run: $(aws ecr get-login --no-include-email)
    - name: Build image
      run: docker build -t ${IMAGE_NAME}:${IMAGE_TAG} .
    - name: Push image
      run: docker push ${IMAGE_NAME}:${IMAGE_TAG}

しかし先日、良さそうなライブラリを教えてもらった。

github.com

Docker Imageのビルドからレポジトリへのログイン・pushまでを行ってくれるライブラリで、push先もDockerHub, GCR, ECR, GitHub Docker Registryに対応している。
ECRにpushする場合は以下のように書ける。

jobs:
  build:
    name: Build and push Docker image
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@master
    - uses: mr-smithers-excellent/docker-build-push@v4
      name: Build & push Docker image
      with:
        image: path/to/ecr/repo
        tag: ${GITHUB_SHA}
        registry: ${{ secrets.CONTAINER_REGISTRY_PATH }}
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

docker image関連のステップ数を大幅に減らすことができ、workflowが見やすくなる。

念のためコードも確認してみる。

github.com

docker関連の処理はdocker.jsに記述されている。ECRの場合 aws ecr get-login でログインを行い、その後 docker build , docker push を行うというよくある処理になっている。

【Kubernetes】Jobの結果をコマンドで取得する

KubernetesのJobのうち、backOffLimit を超えてしまい失敗したもののみを抽出して削除したいときがあった。Jobの結果は type フィールドに入っており、値は CompleteFailed のいずれか。また、Jobが実行中の場合 type フィールド自体が存在しない。

kubectl get job <job_name> -o json
{
    "apiVersion": "batch/v1",
    "kind": "Job",
    ...
    "status": {
        "completionTime": "2020-09-27T03:20:54Z",
        "conditions": [
            {
                "lastProbeTime": "2020-09-27T03:20:54Z",
                "lastTransitionTime": "2020-09-27T03:20:54Z",
                "status": "True", 
                "type": "Complete" # Job結果
            }
        ],
        "startTime": "2020-09-27T03:20:17Z",
        "succeeded": 1
    }
}

このJob結果をコマンドで取りたいと思い、jsonpathでがんばってみた。

kubectl get job $job_name -n $NAMESPACE -o jsonpath='{.status.conditions[].type}'

しかし、これだとtypeフィールドがない場合に対応できない。調べてみたが、jsonpathでは「フィールドがない場合は空の値を返す」という挙動はできないようだ。
最終的にjqコマンドに切り替えて、以下のように実現した。 []? を指定することで、そのフィールドが存在しない場合はnullが返ってくる。

kubectl get job pi -o json | jq '.status.conditions[]?.type' 

Kubectl Cheat Sheetには、jqの方がjsonpathよりも複雑なことができると書いてある。普段使いの場合はjqの方が良いのかもしれない。

jamesdefabia.github.io