永続的なクレデンシャルではなくIAMロールを利用してGithubActionsで認証する

背景

前回、GitHub ActionsでAWS Batchのデプロイを行いました。

AWSの認証では事前にGitHubでSECRETを設定し、GitHub Actionsの中で、ACCESS_KEY_IDSECRET_ACCESS_KEYを設定したSECRETとして利用することで実現していました。

しかし、今は永続的なCredentialsを使うことなく、AWSの認証ができるようです。

https://twitter.com/toricls/status/1438120050167189510

今回は、永続的なCredentialsを用いずにAWSの認証を行ってみようと思います。

IAM Roleの設定

クラスメソッドさんの記事がまとまっていたので、参考にさせていただきました。

GitHub ActionsでAWSの永続的なクレデンシャルを渡すことなくIAM Roleが利用できるようになったようです

ただ、IAM Roleの設定がCloudFormationで書かれていて、CloudFormationに明るくない自分としてはちょっとどうしていいのかわからない状態です。

なので、このCloudFormationの設定をなんとかして、ポチポチで実現しようと思います。

IDプロバイダの追加

まずIDプロバイダの追加を行います。

マネジメントコンソールでIAM -> IDプロバイダを選択します。

  • プロバイダのタイプはOpenID Connectを選択
  • プロバイダのURLにhttps://https://token.actions.githubusercontent.com
  • 対象者にsigstore

と入力し、プロバイダの追加ボタンを押下します。

これでプロバイダの作成は完了です。

IAM Roleの作成

次にロールの作成を行います。

IAM -> IDプロバイダから先程作成したIDプロバイダを選択すると、右上にロールの割り当てというボタンがあるので、押下します。

すると、新しいロールを作成か、既存のロールを使用するを選択できるので、新しいロールを作成を選択します。するとロールの作成画面に遷移します。

ロールの作成画面にて、信頼されたエンティティの種類を選択となっていて、すでにウェブIDが選択されており、IDプロバイダーには先程選択したIDプロバイダーが入力されています。

IDプロバイダ選択

Audienceには、プロバイダーを作成したときに入力したsigstoreが選択できるので、選択して次のステップに移動します。

次はアクセス権限ポリシーをアタッチします。今はテストで行なっていて、この権限の設定で問題が発生すると切り分けが大変なので、一旦強い権限(AdministratorAccess)をアタッチします。

ロールの作成

タグはとばして、ロール名を入力し、ロールの作成ボタンを押下します。

IAM Roleの確認

作成したロールを確認します。

ロールを選択後、信頼関係というタブを押下します。そして、信頼関係の編集というボタンを押下すると、JSON形式で確認できます。

以下のようになっているとOKです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::xxxxxxxxxxxx:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sigstore"
}
}
}
]
}

これでIAM Roleの設定は完了しました。

GitHub Actionsの設定

IAM Roleの準備が整ったので、GitHub Actionsの設定を行います。

基本的には、クラスメソッドさんの記事そのまま実装すれば大丈夫そうです。

以下に実装例を記述します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
name: Deploy to AWS Batch

on:
push:
branches:
- dev

env:
AWS_REGION: ap-northeast-1
AWS_ROLE_ARN: arn:aws:iam::xxxxxxxxxxxx:role/GithubActionsDeployRole
AWS_WEB_IDENTITY_TOKEN_FILE: /tmp/awscreds
ECR_REPOSITORY: test-repository
BATCH_JOB_DEFINITION: aws/batch/job-definition.json
BATCH_JOB_DEFINITION_TEMPLATE: aws/batch/job-definition-template.json

jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
environment: production
permissions:
id-token: write
contents: read

steps:
- name: Checkout
uses: actions/checkout@v2

- name: Configure AWS
run: |
echo AWS_WEB_IDENTITY_TOKEN_FILE=$AWS_WEB_IDENTITY_TOKEN_FILE >> $GITHUB_ENV
echo AWS_ROLE_ARN=$AWS_ROLE_ARN >> $GITHUB_ENV
echo AWS_DEFAULT_REGION=$AWS_REGION >> $GITHUB_ENV
curl --silent -H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=sigstore" | jq -r '.value' > $AWS_WEB_IDENTITY_TOKEN_FILE

- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1

Login to Amazon ECR以降は永続的なCredentialsを使っている時と同じです。

こちらでActionsを起動して、コンテナイメージのpush、ジョブ定義ファイルのデプロイが完了しました。

ハマったポイント

ハマったポイントとしましては、最初、GitHub Actions内で参照している環境変数ACTIONS_ID_TOKEN_REQUEST_TOKENACTIONS_ID_TOKEN_REQUEST_URLの値が取れておらずなんでだろうと考えていましたが、結果としては

1
2
3
permissions:
id-token: write
contents: read

この設定が漏れていました…

id-tokenに対して、書き込む権限がないと、上記の環境変数は空になってしまうようです。

まとめ

永続的なCredentialsを使うことなくGitHub ActionsにてAWSの認証を行うことができました。

正式な発表はないらしいですが、GitHub Actionsを利用するためだけに専用ユーザーを作成し、永続的なCredentialsを発行してGitHubに設定するという、セキュリティ的に良くないことをしなくてよくなることを考えると、この方法が今後はデファクトになっていくんだろうなと思います。

次回は、先程の環境変数が取得できずに困っていたときに利用した、GitHub ActionsでのSSHデバッグについて記載したいと思います。

GitHub Actionsを使ってAWS Batchをデプロイする

背景

前回、AWS Batchのジョブ定義ファイルを取得するため、AWS CLIでスイッチロール先のAWSアカウントのCredentialsの設定を行いました。

今回はそのジョブ定義ファイルを使って、GitHub ActionsでAWS Batchのデプロイを行いたいと思います。

Batchのデプロイ

Batchのデプロイの範囲ですが、今回は以下の部分を対象とします。

  • ECRにログイン
  • Batch実行コンテナイメージのビルド
  • Batch実行コンテナイメージをECRに登録
  • ジョブ定義ファイル内に記載したコンテナイメージのURLを上記で登録したコンテナイメージのURLで置き換え
  • ジョブ定義ファイルのデプロイ

GitHub Actionsの実装

ではGitHub Actionsを実装していきます。環境変数が至る所で参照されていますが、jobsの上あたりで全て定義してあります。また、GitHub上でsecretsを登録しております。

ECRにログイン

ECRへのログインはAmazon ECSのテンプレートと同じ形で実行できます。

1
2
3
4
5
6
7
8
9
10
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}

- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1

ここではまだ永続的なCredentialsを使っていますが、近いうちにIAM Roleを利用した一時的なCredentialsに置き換えようと思います。

コンテナイメージのビルド、ECRへ登録

コンテナイメージのビルドとECRへの登録もAmazon ECSのテンプレートと同じ形で実行できます。

1
2
3
4
5
6
7
8
9
- name: Build, tag, and push image to Amazon ECR
id: build-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"

最後に、コンテナイメージのURLを出力しています。

ジョブ定義ファイルの更新

次にジョブ定義ファイルの更新を行います。

ジョブ定義ファイル内のコンテナイメージのURLを新しくECRに登録したコンテナイメージのURLに差し替える必要があります。

いろんな方法があると思いますが、わたしは前回取得したジョブ定義ファイルをテンプレートファイルとしてGit管理しておき、そのテンプレートファイルのコンテナイメージURLの部分だけ置換するような方法にしました。

1
2
3
4
5
6
- name: Fill in the new image ID in the AWS Batch job definition
id: task-def
env:
IMAGE: ${{ steps.build-image.outputs.image }}
run: |
sed -e "s#__image__#$IMAGE#" $BATCH_JOB_DEFINITION_TEMPLATE > $BATCH_JOB_DEFINITION

ジョブ定義ファイル内のコンテナイメージの箇所は__image__としておき、それをECRに登録したコンテナイメージのURLに置き換え、ファイルとして保存します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"jobDefinitionName": "test-batch",
"type": "container",
"parameters": {},
"retryStrategy": {
"attempts": 1,
"evaluateOnExit": []
},
"containerProperties": {
"image": "__image__",
"command": [
"echo",
"'hello world'"
],

コマンドの箇所はplaceholderが利用できるようですが、コマンドの箇所だけのようです。
https://docs.aws.amazon.com/ja_jp/batch/latest/userguide/job_definition_parameters.html#parameters

これで新しいコンテナイメージのジョブ定義ファイルが作成できました。

ジョブ定義ファイルのデプロイ

最後にジョブ定義ファイルのデプロイを行います。

1
2
3
4
- name: Deploy AWS Batch job definition
id: deploy-job-def
run: |
aws batch register-job-definition --cli-input-json file://$BATCH_JOB_DEFINITION

aws batch register-job-definitionコマンドを用いて、ジョブ定義ファイルを登録します。そうすることで新しいジョブ定義ファイルが有効になります。

異常で、AWS Batchのデプロイは完了です。

まとめ

今回はAWS BatchをGitHub Actionsを用いてデプロイしました。AWS BatchのトリガーとしてAmazon EventBridgeを利用しているのであれば、EventBridge側の変更も必要になるかもしれません。こちらは近いうちに調査してみます。

また、今回はaccess_key_id, secret_access_keyという永続的なCredentialsを利用しましたが、セキュリティ的に永続的なCredentialsを利用するのは好ましくありません。次回は一時的なCredentialsを利用してAWS Batchをデプロイするよう修正したいと思います。

xmlのparseで利用するためにlxmlをインストールする

背景

以前はxmlのparseにxml.etree.ElementTreeを利用しました。実際に利用したところ、ファイルサイズが大きめのXMLを読み込んでループを回したところで、out of memoryになり、停止してしまいました。

なにか別のライブラリはないかなと思って探したところlxmlというパッケージが見つかったのでそれを利用してみようと思います。

インストール

pipでインストールします。Pipfilelxml = "*"を追加して、インストールします。

1
2
3
4
5
6
7
8
9
$ docker-compose run --rm web python -m pipenv install --system --skip-lock
Creating test_site_web_run ... done
Installing dependencies from Pipfile...
🐍 ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 1/1 — 00:00:00
An error occurred while installing lxml! Will try again.
Installing initially failed dependencies...
(中略)
[pipenv.exceptions.InstallError]: error: command 'gcc' failed: No such file or directory
...

エラーが起こってしまいました。'gcc' failed: No such file or directoryというエラーになっているので確認してみると、gccがインストールされていません。

gccのインストール

Dockerfileに以下の1行を追記します。

1
RUN apk add gcc

再ビルドします。

1
2
3
4
5
6
7
$ docker-compose build                                                     
Building web
[+] Building 23.5s (12/12) FINISHED
(中略)
#12 20.56 [pipenv.exceptions.InstallError]: /tmp/xmlXPathInitzi6405c3.c:1:10: fatal error: libxml/xpath.h: No such file or directory
#12 20.56 [pipenv.exceptions.InstallError]: 1 | #include "libxml/xpath.h"
...

xpath.hがないということです。調べてみると、libxslt-devというパッケージが必要とのことなので、以下のように修正してインストールします。

1
RUN apk add gcc libxslt-dev

ビルドします。

1
2
3
4
5
6
7
8
9
$ docker-compose build
Building web
(中略)
#12 11.11 [pipenv.exceptions.InstallError]: /usr/local/include/python3.9/Python.h:11:10: fatal error: limits.h: No such file or directory
#12 11.11 [pipenv.exceptions.InstallError]: 11 | #include <limits.h>
(中略)
#12 11.11 [pipenv.exceptions.InstallError]: /usr/include/libxml2/libxml/tree.h:15:10: fatal error: stdio.h: No such file or directory
#12 11.11 [pipenv.exceptions.InstallError]: 15 | #include <stdio.h>
...

また失敗しました。ちょっとわからないので、調べてみます。

musl-devというパッケージが必要とのことなので、そちらもインストールします。

1
RUN apk add gcc libxslt-dev musl-dev

もう一度ビルドします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ docker-compose build
Building web
[+] Building 185.8s (13/13) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 260B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/python:3.9.7-alpine 2.5s
=> [1/8] FROM docker.io/library/python:3.9.7-alpine@sha256:eb1b2038f12c8916be54329319a625de2dfec4266b718efdb798cc149b342a2f 0.0s
=> [internal] load build context 0.1s
=> => transferring context: 18.50kB 0.1s
=> CACHED [2/8] RUN apk update 0.0s
=> [3/8] RUN apk add gcc libxslt-dev musl-dev 4.6s
=> [4/8] RUN mkdir /code 0.4s
=> [5/8] WORKDIR /code 0.0s
=> [6/8] ADD . /code/ 0.1s
=> [7/8] RUN pip install pipenv 8.1s
=> [8/8] RUN python -m pipenv install --system --skip-lock 168.1s
=> exporting to image 1.8s
=> => exporting layers 1.8s
=> => writing image sha256:499cdeca8be5a7281b86e0e691498568fef3dec05495c64552e99aea0322d53e 0.0s
=> => naming to docker.io/library/test_site_web 0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them

無事ビルドできました!

まとめ

lxmlを利用するには、gcc, libxslt-dev, musl-devが必要でした。

次回はlxmlを使ってparseする処理を実装していきたいと思います。

参考図書

停止しているコンテナのイメージを削除する

背景

dockerイメージを整理しようと思い、docker imagesコマンドを実行したら相当な数のイメージが表示されました。

REPOSITORYとTAGの両方が<none>になっているイメージが多々あったので削除しようと思います。

イメージの削除

ではイメージの削除を行います。docker rmiコマンドを使います。

1
2
$ docker rmi d0df5002f11a
Error response from daemon: conflict: unable to delete d0df5002f11a (must be forced) - image is being used by stopped container ed33132b39b4

エラーが返ってきてしまいました。メッセージを見ると、消そうとしたイメージは止まっているコンテナ(コンテナID ed33132b39b4)で利用されているということです。

コンテナの状況確認

コンテナの状況を確認します。止まっているコンテナも表示したいので、docker ps -aを実行します。

1
2
3
4
5
6
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
bbe9ff0a5ec5 my-ruby-app "bundle exec ruby ./…" 4 days ago Exited (0) 4 days ago relaxed_herschel
0af5cf244837 my-ruby-app "bundle exec ruby ./…" 4 days ago Exited (0) 4 days ago peaceful_carson
8216a78943ca d0df5002f11a "bundle exec ruby ./…" 4 days ago Exited (0) 4 days ago nervous_mclean
ed33132b39b4 d0df5002f11a "bundle exec ruby ./…" 4 days ago Exited (0) 4 days ago hungry_kepler

こちらの記事で利用していたコンテナが止まった状態で残ってしまっているようです。実行時に--rmオプションをつけ忘れたのでしょうか。

ではこれらのコンテナを削除します。

コンテナの削除

コンテナの削除にはdocker rmコマンドを利用します。コンテナIDを指定して実行します。

1
2
$ docker rm ed33132b39b4
ed33132b39b4

確認します。

1
2
3
4
5
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
bbe9ff0a5ec5 my-ruby-app "bundle exec ruby ./…" 4 days ago Exited (0) 4 days ago relaxed_herschel
0af5cf244837 my-ruby-app "bundle exec ruby ./…" 4 days ago Exited (0) 4 days ago peaceful_carson
8216a78943ca d0df5002f11a "bundle exec ruby ./…" 4 days ago Exited (0) 4 days ago nervous_mclean

削除できているので、もう一度イメージの削除を行います。

1
2
$ docker rmi d0df5002f11a
Error response from daemon: conflict: unable to delete d0df5002f11a (must be forced) - image is being used by stopped container 8216a78943ca

別のコンテナも利用しているようです…このコンテナも削除します。

1
2
$ docker rm 8216a78943ca
8216a78943ca

イメージを削除します。

1
2
$ docker rmi d0df5002f11a
Deleted: sha256:d0df5002f11a7a3ae48c2567ae218af364facdb8916f93589fc39576815d045d

無事削除できました。

まとめ

イメージを削除できない原因は止まっているコンテナが利用しているイメージだったからでした。

docker run実行時のオプションに--rmを指定しておけば、コンテナ終了時に自動的にコンテナも削除されるので、まずはこのオプションを指定しておくのが良さそうです。

オプションを指定し忘れた場合はdocker rmコマンドで終了させます。そうすればイメージも削除できるはずです。

次回はREPOSITORYやTAGが<none>になってしまうのはなぜなのかを調べてみたいと思います。

参考図書

スイッチロールユーザーのcredentialsの設定

背景

AWS BatchをGithubActionsでデプロイするために、ジョブ定義ファイルを取得しようとしました。マネジメントコンソールでは取得できないようでした…

aws cliを用いて取得するしかないと思ったのですが、AWS Batchの設定を行ったのは、スイッチロール先のAWSアカウントです。

スイッチロールしたIAMでジョブ定義ファイルを取得するための設定メモを記載します。

configファイルの編集

スイッチロール先のAWSアカウントのprofileの設定を~/.aws/configファイルに行います。

1
2
3
4
5
[profile switch-role-to]
region = ap-northeast-1
output = json
source_profile = switch-role-from
role_arn = arn:aws:iam::スイッチロール先アカウントID:role/スイッチロール

他の設定と異なるところはsource_profilerole_arnの設定です。

source_profileには、スイッチロール元のprofile名を設定します。role_arnにはスイッチロール先のロールを指定します。

アクセスを試す

それではアクセスできるかどうか試してみましょう。

1
2
3
$ aws batch describe-job-definitions --profile switch-role-to

An error occurred (AccessDenied) when calling the AssumeRole operation: User: arn:aws:iam::スイッチロール元アカウントID:user/スイッチロール元IAMユーザー名 is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::スイッチロール先アカウントID:role/スイッチロール

ん〜エラーになってしまいました。ググってみるとMFAの設定をしているとこのメッセージが表示されるようです。

MFA対応

MFAに対応するために、~/.aws/configに以下の内容を追記しましょう。

1
mfa_serial = arn:aws:iam::スイッチロール元アカウントID:mfa/スイッチロール元IAMユーザー名

再度試してみます。

1
2
$ aws batch describe-job-definitions --profile switch-role-to
Enter MFA code for arn:aws:iam::スイッチロール元アカウントID:mfa/スイッチロール元IAMユーザー名 :

MFAのコードを聞かれているので、入力します。するとジョブ定義ファイルが正常に表示されました。この設定の仕方だと、すべてのジョブ定義ファイルが全てのリビジョンにおいて出力されます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ aws batch describe-job-definitions --profile switch-role-to
Enter MFA code for arn:aws:iam::スイッチロール元アカウントID:mfa/スイッチロール元IAMユーザー名 :
{
"jobDefinitions": [
{
"jobDefinitionName": "test",
...
(中略)
...
"propagateTags": false,
"platformCapabilities": [
"FARGATE"
]
}
]
}

無事取得することができました!

まとめ

スイッチロール先のAWSアカウントにAWS CLIでアクセスする方法をまとめました。

次回は取得したジョブ定義ファイルを使ってデプロイを試したいと思います。

簡単なRubyスクリプトをDockerで実行する

背景

データの修正のために、ちょっとしたバッチを書くことになりました。環境はEC2でAmazon Linux2でした。

rbenvをインストールしようかなと思って準備をし、いざrubyをインストールしようとしたところで、gccが入っておらずコンパイルができないことに気がつきました…

いつもならyum install -y gccなど行うところですが、パッケージの追加もあまりしてほしくないということだったので、Dockerコンテナで実行することにしました。

Dockerfileの作成

まずはDockerfileを作成します。

EC2上に適当なディレクトリを作成して、その中にDockerfileを作成します。

1
2
3
$ mkdir app
$ cd app
$ touch Dockerfile

ベースイメージの取得

それではDockerfileのFROMに記載するベースイメージを決めようと思います。

Docker image Rubyでググると、Rubyのオフィシャルイメージが見つかります。

https://hub.docker.com/_/ruby

以前Rubyコミッタの方が最新のバージョンを使うのが速度的には一番良いとおっしゃっていたので、最新の3.0.2を利用します。

Dockerfileつづき

先程のRubyのオフィシャルイメージのページの下の方を見ていくと、How to use this imageというセクションがあり、実際の利用方法が記載されています。

例に書いてあるのを倣って作成し一度ビルドしてみます。Dockerfileは以下のようになりました。

1
2
3
4
5
6
7
8
9
10
11
$ cat Dockerfile
FROM ruby:3.0.2

RUN bundle config --global frozen 1

WORKDIR /usr/src/app

COPY Gemfile Gemfile.lock ./
RUN bundle install

CMD ["bundle", "exec", "ruby", "./test.rb"]

Gemfileは今回必要となるデータベース(MySQL RDS)への接続と、その接続のための認証情報を取得するためのgemのみ記載しました。

1
2
3
4
5
$ cat Gemfile
source 'https://rubygems.org'

gem 'mysql2'
gem 'dotenv'

イメージのビルド

ではこれでビルドします

1
2
3
4
5
6
7
8
9
10
11
$ docker build -t my-ruby-app .
[+] Building 5.0s (8/11)
=> [internal] load build definition from Dockerfile 0.0s
...
(中略)
...
=> ERROR [4/7] COPY Gemfile Gemfile.lock ./ 0.0s
------
> [4/7] COPY Gemfile Gemfile.lock ./:
------
failed to compute cache key: "/Gemfile.lock" not found: not found

Gemfile.lockはなかったですね。Dockerfileの7行目からGemfile.lockを削除して再実行します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ docker build -t my-ruby-app .
[+] Building 5.4s (9/11)
=> [internal] load build definition from Dockerfile 0.0s
...
(中略)
...
=> CACHED [4/7] COPY Gemfile ./ 0.0s
=> ERROR [5/7] RUN bundle install 1.0s
------
> [5/7] RUN bundle install:
#9 1.002 The deployment setting requires a Gemfile.lock. Please make sure you have
#9 1.002 checked your Gemfile.lock into version control before deploying.
------
executor failed running [/bin/sh -c bundle install]: exit code: 16

エラーになってしまいました。調べてみると、Dockerfileの3行目にある

1
RUN bundle config --global frozen 1

で、GemfileやGemfile.lockの変更を禁止しているからのようです。

https://bundler.io/v2.2/man/bundle-config.1.html

その行を削除しても良いのですが、一般的にimageのビルドはそのビルドしたimageをデプロイして動作させるので、デプロイの一環でもあります。上記のbundlerのマニュアルにもある通り、--deploymentフラグを有効にすると、BUNDLE_FROZENtrueになるようですし、Gemfile.lockはソースコード管理されているはずなので、Dockerfileはこのままでいきます。

Gemfile.lockの作成

ではGemfile.lockはどのように作成すれば良いでしょうか。

先程のDockerHubのページに作成方法が書いてありました。

1
2
3
4
Generate a Gemfile.lock
The above example Dockerfile expects a Gemfile.lock in your app directory. This docker run will help you generate one. Run it in the root of your app, next to the Gemfile:

$ docker run --rm -v "$PWD":/usr/src/app -w /usr/src/app ruby:2.5 bundle install

イメージのバージョンだけ現在の3.0.2に合わせて実行してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ docker run --rm -v "$PWD":/usr/src/app -w /usr/src/app ruby:3.0.2 bundle install
Unable to find image 'ruby:3.0.2' locally
3.0.2: Pulling from library/ruby
...
(中略)
...
Status: Downloaded newer image for ruby:3.0.2
Fetching gem metadata from https://rubygems.org/..
Resolving dependencies...
Using bundler 2.2.22
Fetching mysql2 0.5.3
Fetching dotenv 2.7.6
Installing dotenv 2.7.6
Installing mysql2 0.5.3 with native extensions
Bundle complete! 2 Gemfile dependencies, 3 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

無事Gemfile.lockが作成できました。

再ビルド

ではもう一度ビルドしてみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ docker build -t my-ruby-app .
[+] Building 11.6s (12/12) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 236B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/ruby:3.0.2 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 641B 0.0s
=> [1/7] FROM docker.io/library/ruby:3.0.2 0.0s
=> CACHED [2/7] RUN bundle config --global frozen 1 0.0s
=> CACHED [3/7] WORKDIR /usr/src/app 0.0s
=> [4/7] COPY Gemfile Gemfile.lock ./ 0.0s
=> [5/7] RUN bundle install 11.2s
=> [6/7] COPY .env ./ 0.0s
=> [7/7] COPY . . 0.0s
=> exporting to image 0.2s
=> => exporting layers 0.1s
=> => writing image sha256:a67fb078890bb0301ed294fed21fe1d349a1ab731604328470ac759 0.0s
=> => naming to docker.io/library/my-ruby-app 0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them

イメージを確認します

1
2
$ docker images | grep -i ruby-app
my-ruby-app latest a67fb078890b 40 seconds ago 906MB

ちょっとサイズが大きい気がしますが、ビルドできました。

スクリプトの実行

では実行します。なにも指定しなければ、Dockerfileで最後に記述したコマンドが実行されます。

1
$ docker run my-ruby-app

実行ログなどを出力したい場合は、Gemfile.lockを作成した時のようにログを置くディレクトリを-vでマウントします。

test.rb

1
2
3
4
5
require 'dotenv'
require 'mysql2'

log = File.open('test.log', 'w')
log.write('log message')
1
2
3
$ docker run -v "$PWD":/usr/src/app my-ruby-app
$ cat test.log
log message

無事書き込みできていました。

まとめ

環境を変更することなく実行できるコンテナの長所を利用するといろんなことができるようになると思います。

これからはなにかを行うにしても1から実行環境を整えることもなくなりそうですね。便利な世の中になりました。

Python3でxmlをparseする

背景

構造化されたデータを読み込むことになり、そのデータが特定のURLにアクセスするとxml形式で取得できることがわかりました。

Python3をあまり知らないこともあり、同じような処理をこれからも行っていく予定なので、まとめようと思います。

HTTPクライアント

特定のURLにアクセスするためにHTTPクライアントを生成する必要があります。

私の知っている言語だとHTTPクライアントはいろんなライブラリがあったりして、それぞれ一長一短があります。Python3ではどうかなと思い、入門Python3を調べてみると、標準ライブラリurllib.requestが紹介された後に、requestsが紹介されていて、ほとんどの目的ではrequestsを使った方がウェブ開発が簡単になるようだと記載されています。わたしも倣ってrequestsを使ってみようと思います。

requestsのインストール

まずはじめにインストールする必要があります。

現在インストールされているパッケージを確認しましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ docker-compose run --rm web python -m pip list
Creating network "django_default" with the default driver
Creating django_web_run ... done
Package Version
--------------------------------- ---------
asgiref 3.4.1
backports.entry-points-selectable 1.1.0
boto3 1.18.38
botocore 1.21.38
certifi 2021.5.30
distlib 0.3.2
Django 3.2.7
filelock 3.0.12
jmespath 0.10.0
pip 21.2.4
pipenv 2021.5.29
platformdirs 2.3.0
python-dateutil 2.8.2
pytz 2021.1
s3transfer 0.5.0
setuptools 57.5.0
six 1.16.0
sqlparse 0.4.1
urllib3 1.26.6
virtualenv 20.7.2
virtualenv-clone 0.5.7
wheel 0.37.0

インストールされていないようなのでインストールします。Pipfileに追記します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
Django = "==3.2.7"
boto3 = "*"
requests = "*"

[dev-packages]

[requires]
python_version = "3.9"

インストールします。

1
2
3
4
$ docker-compose run --rm web python -m pipenv install --system --skip-lock
Creating django_web_run ... done
Installing dependencies from Pipfile...
🐍 ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 1/1 —

インストールされたかどうか確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ docker-compose run --rm web python -m pip list
Creating django_web_run ... done
Package Version
--------------------------------- ---------
asgiref 3.4.1
backports.entry-points-selectable 1.1.0
boto3 1.18.38
botocore 1.21.38
certifi 2021.5.30
charset-normalizer 2.0.6
distlib 0.3.2
Django 3.2.7
filelock 3.0.12
idna 3.2
jmespath 0.10.0
pip 21.2.4
pipenv 2021.5.29
platformdirs 2.3.0
python-dateutil 2.8.2
pytz 2021.1
requests 2.26.0
s3transfer 0.5.0
setuptools 57.5.0
six 1.16.0
sqlparse 0.4.1
urllib3 1.26.6
virtualenv 20.7.2
virtualenv-clone 0.5.7
wheel 0.37.0

無事インストールされました。

requestsの基本的な使い方

次に基本的な利用方法を見ていきましょう。試すだけなのでDjango Shellを利用します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ docker-compose run --rm web python manage.py shell
Creating django_web_run ... done
Python 3.9.7 (default, Aug 31 2021, 19:01:35)
[GCC 10.3.1 20210424] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> import requests
>>> res = requests.get('https://google.co.jp')
>>> res.status_code
200
>>> res.headers['content-type']
'text/html; charset=Shift_JIS'
>>> res.encoding
'Shift_JIS'
>>> res.text
'<!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" lang="ja"><head><meta content="世界中のあらゆる情報を検索するためのツールを提供しています。さまざまな検索機能を活用して、お探しの情報を見つけてください。" name="description">...(省略)

HTTPクライアントを作成し、コンテンツを取得できました。requestsはかなり直感的でわかりやすいと思います。では実際にXMLコンテンツを取得し、parseしたいと思います。

XML parserの選定

python xml parserでググるとxml.etree.ElementTreeというライブラリが出てきます。

ドキュメント: https://docs.python.org/ja/3/library/xml.etree.elementtree.html

これは標準ライブラリのようなのでインストールは不要でimportすれば利用できます。

サンプルのXMLを使って、試してみましょう。

1
2
3
4
5
6
7
8
>>> import requests
>>> import xml.etree.ElementTree as ET
>>> res = requests.get('https://www.w3schools.com/xml/simple.xml')
>>> res.status_code
200
>>> root = ET.fromstring(res.text)
>>> root.tag
'breakfast_menu'

XMLのサンプルは

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<breakfast_menu>
<food>
<name>Belgian Waffles</name>
<price>$5.95</price>
<description>Two of our famous Belgian Waffles with plenty of real maple syrup</description>
<calories>650</calories>
</food>
<food>
<name>Strawberry Belgian Waffles</name>
<price>$7.95</price>
<description>Light Belgian waffles covered with strawberries and whipped cream</description>
<calories>900</calories>
</food>
<food>
<name>Berry-Berry Belgian Waffles</name>
<price>$8.95</price>
<description>Light Belgian waffles covered with an assortment of fresh berries and whipped cream</description>
<calories>900</calories>
</food>
<food>
<name>French Toast</name>
<price>$4.50</price>
<description>Thick slices made from our homemade sourdough bread</description>
<calories>600</calories>
</food>
<food>
<name>Homestyle Breakfast</name>
<price>$6.95</price>
<description>Two eggs, bacon or sausage, toast, and our ever-popular hash browns</description>
<calories>950</calories>
</food>
</breakfast_menu>

なので、正しくrootが取得できています。

また、先程のrootはイテレート可能な子ノードを持ちます。

1
2
3
4
5
6
7
8
>>> for child in root:
... print(child.tag, child.attrib)
...
food {}
food {}
food {}
food {}
food {}

上から構造化データを辿っていくことが可能です。

データ構造を調査する

上記の例で言うと、foodの下の階層の要素がわからない場合はどうしたらよいでしょうか?

そう言った場合は先程のrootと同じように以下のようにすると下の階層の要素のtagがわかります。

1
2
3
4
5
6
7
8
>>> foods = root.findall('food')
>>> for e in foods[0]:
... print(e.tag)
...
name
price
description
calories

そうすると、どう言ったtagを持った要素が存在するのかわかります。tagがわかればtag名でアクセス可能です。

1
2
>>> foods[0].find('name').text
'Belgian Waffles'

まとめ

Python3でHTTPクライアントとXMLパーサを触ってみました。

HTTPクライアントのrequestsはとても利用しやすく、直感的にわかる感じのパッケージでした。今後もrequestsを利用していこうと思います。

XMLパーサについては直感的に理解するのが難しく、Elementの要素に対してアクセスするのも一苦労でした。利用しながら慣れていこうと思います。

参考図書

Githubでローカルのリポジトリをリモートに作成する方法

背景

ローカルでコツコツ実装していたリポジトリを周りのメンバーに共有するためにGithubにpushしたりするときもあると思います。

たまにしか行わない作業で忘れがちなのでメモしておきます。

リモートにリポジトリを作成する

Githubにログインしてリポジトリを作成します。

わたしはだいたい~/github/リポジトリ名というディレクトリを作成しているので、このディレクトリに合わせたリポジトリ名にします。

リモートにリポジトリをpushする

リポジトリ作成後に次のアクションの例が表示されるので、それを参考にします。

ローカルになにもない場合

作成したリポジトリをクローンしてからcommit, pushします。
SSH or HTTPSでクローンします

コマンドラインで新しいリポジトリを作成する場合

クローンするのではなく、ローカルでディレクトリを作成してリポジトリとします。

1
2
3
4
5
6
7
echo "# リポジトリ名" >> README.md
git init
git add README.md
git commit -m "first commit"
git branch -M main
git remote add origin https://github.com/オーガナイゼーション名/リポジトリ名.git
git push -u origin main

既存のリポジトリをコマンドラインでpushする場合

今回のケースはこちらにあたります。

1
2
3
git remote add origin https://github.com/オーガナイゼーション名/リポジトリ名.git
git branch -M main
git push -u origin main

-Mオプションってなんでしたっけと思ったので調べてみました。

1
2
3
4
With a -m or -M option, <oldbranch> will be renamed to <newbranch>.
If <oldbranch> had a corresponding reflog, it is renamed to match <newbranch>,
and a reflog entry is created to remember the branch renaming.
If <newbranch> exists, -M must be used to force the rename to happen.

(見やすいよう改行を入れています)

既存のブランチ名をmainに変えてくれってことみたいですね。

いつからこのようになったのでしょうか?調べてみると以下の記事が出てきました。

GitHub、これから作成するリポジトリのデフォルトブランチ名が「main」に。「master」から「main」へ変更

1年くらい前からそうなってたんですね…知りませんでした…

ということで、今後はmainにしていこうと思います。

別のリポジトリからインポートする

リポジトリのURLを指定することで既存のリポジトリをインポートできるようです。

実際にやってみる

では先程作成したGithubのリポジトリをリモートに設定します。

まず現在の設定状況を確認します

1
$ git remote -v

なにも表示されないので設定はされていません。

ではリモートを追加します。

1
$ git remote add origin https://github.com/オーガナイゼーション名/リポジトリ名.git

追加できているか確認します。

1
2
3
$ git remote -v
origin https://github.com/オーガナイゼーション名/リポジトリ名.git (fetch)
origin https://github.com/オーガナイゼーション名/リポジトリ名.git (push)

設定できました。

ではブランチ名をmainに変更します。

1
2
$ git branch
* master

今はmasterのみです。

1
2
3
$ git branch -M main
$ git branch
* main

変更できました。

最後にpushします。

1
2
3
$ git push -u origin main
remote: Repository not found.
fatal: repository 'https://github.com/オーガナイゼーション名/リポジトリ名.git/' not found

うーん、プライベートリポジトリだから認証がうまくいってないようですね。remoteのURLにユーザー名を追加してみます。

リモートを追加するには一度削除してから追加します。

1
2
$ git remote rm origin
$ git remote add origin https://ユーザー名@github.com/オーガナイゼーション名/リポジトリ名.git

もう一度pushしてみます。

1
2
3
4
5
6
7
8
9
10
11
$ git push -u origin main
Enumerating objects: 45, done.
Counting objects: 100% (45/45), done.
Delta compression using up to 8 threads
Compressing objects: 100% (39/39), done.
Writing objects: 100% (45/45), 7.56 KiB | 1.51 MiB/s, done.
Total 45 (delta 17), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (17/17), done.
To https://github.com/オーガナイゼーション名/リポジトリ名.git
* [new branch] main -> main
Branch 'main' set up to track remote branch 'main' from 'origin'.

無事pushできました。

まとめ

最初は自分だけで開発している場合ものちにGithubリポジトリにpushしてチーム開発していくことが多くあると思います。

いつでもpushできるよう準備しておくのが良さそうですね。

参考図書

複数のgitアカウントの利用方法

背景

Macのリプレースを行って(最近はこればっかり)、git関連の設定もふっとんでいます。

Macでは、会社用・プライベート用などいろんなgithubのアカウントを利用しています。複数のgitアカウントを設定する時のわかりやすい方法を調べました。

普段は1度設定したらあまり設定することのないGitの設定をまとめたいと思います。

事前準備 git: ‘secrets’ is not a git command. See ‘git –help’.

Macで一番最初にgit commitしようとしたら、エラーが発生しました

1
git: 'secrets' is not a git command. See 'git --help'.

ググってみると、git-secretsをインストールする必要があるようでした。

1
$ brew install git-secrets

こちらでエラーはなくなります。

設定の方針

設定の方針としてはメインで利用するアカウントをglobalに設定し、それをオーバーライドしたいリポジトリではlocalでアカウントを設定します。

そうすることで、よく使うアカウントがデフォルトの設定になり、手間が省けます。

よく利用するアカウントの設定

会社のPCだと、会社のGithubのアカウントを利用することが多いです。なので、会社のGithubアカウントをglobalに設定します。

まず現在の設定を確認してみます。

1
2
$ git config --global --list
fatal: unable to read config file '/Users/user/.gitconfig': No such file or directory

そもそもファイルがありませんでした…コマンドでuser.nameuser.emailを設定してみます。

1
2
$ git config --global user.name "会社のアカウント名"
$ git config --global user.email "会社のメールアドレス"

反映されていることを確認します

1
2
3
4
$ git config --list
credential.helper=osxkeychain
user.name=会社のアカウント名
user.email=会社のメールアドレス

プライベートのアカウントの設定

プライベートなGithubアカウントを設定したいリポジトリのディレクトリに移動します。

そこでgit config --local --listを実行します。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git config --local --list
user.name=会社のアカウント名
user.email=会社のメールアドレス
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
core.ignorecase=true
core.precomposeunicode=true
remote.origin.url=https://github.com/プライベート/xxxxxxx.git
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
branch.master.remote=origin
branch.master.merge=refs/heads/master

globalで設定した会社のメールアドレスが設定されています。ここで、git config --localを利用して、アカウント名とメールアドレスを設定してみます。

1
2
$ git config --local user.name "プライベートのアカウント名"
$ git config --local user.email "プライベートのメールアドレス"

反映されていることを確認します

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git config --local --list
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
core.ignorecase=true
core.precomposeunicode=true
remote.origin.url=https://github.com/プライベートのアカウント名/xxxxxxx.git
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
branch.master.remote=origin
branch.master.merge=refs/heads/master
user.email=プライベートのメールアドレス
user.name=プライベートのアカウント名

正しく反映されていました。

設定は~/.git/configに保存されています。確認してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ cat .git/config 
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = https://github.com/プライベートのアカウント名/xxxxxxx.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
[user]
email = プライベートのメールアドレス
name = プライベートのアカウント名

しっかり保存されていました。

git commitして動作確認

実際にgit commitして、ログメッセージがどうなるかを確認します。

1
2
3
4
5
6
7
8
9
$ git commit -m "git commitのテストのためにコミット"
[mastet xxxx] git commitのテストのためにコミット
1 file changed, .....
$ git log
commit xxxxxxxxxxxx (HEAD -> master)
Author: プライベートのアカウント名 <プライベートのメールアドレス>
Date: Fri Sep 10 23:23:58 2021 +0900

git commitのテストのためにコミット

アカウント名、メールアドレスともに正しく認識されています。

これでgitの複数アカウントでの設定が完了しました。

おまけ

gitの設定がされていない場合、アカウント名はMacのログイン名、メールアドレスはMacのアカウント@ホスト名となります。

まとめ

Gitの設定はスムーズに開発を行うために必須です。アカウントを正しく設定できていないと、Githubでコミットログでも正しいアカウントがヒョ次されていることが好ましいです(誰による変更かわからなくなってしまうため)

今回基本的な設定を行いましたが、これからもgitの設定を育てていきたいと思います。

参考図書

pyenvを使ってVSCodeで利用するpythonのバージョンを指定する

背景

Macのリプレースを行ったときにVSCodeの設定の引き継ぎを行わなかったので、最初から設定することになってしまいました。

Pythonのインストールとバージョンの切り替えにはpyenvを利用していたので、引き続き使っていこうと思っていましたが、VSCodeのpythonプラグインをインストールした後のpython実行ファイルの指定で少し詰まってしまったことを記載します。

pythonプラグインのインストール

VSCodeで.pyのファイルを開くと、VSCodeからpythonプラグインのインストールを勧められるので、そのままインストールします。

インタプリタの設定

プラグインのインストールが終わると、インタプリタの設定画面が表示されます。Pythonをインストールする、インタプリタを選ぶなどの選択肢がありますが、pyenvでPythonをインストールしていなかったのでまずはpyenvでPythonをインストールします。

pyenvでPythonをインストールする

以前から利用していた3.9.6をインストールします(現時点での最新版は3.9.7)

1
2
3
4
5
6
7
8
9
$ pyenv install 3.9.6
python-build: use openssl@1.1 from homebrew
python-build: use readline from homebrew
Downloading Python-3.9.6.tar.xz...
-> https://www.python.org/ftp/python/3.9.6/Python-3.9.6.tar.xz
Installing Python-3.9.6...
python-build: use readline from homebrew
python-build: use zlib from xcode sdk
Installed Python-3.9.6 to /Users/user/.anyenv/envs/pyenv/versions/3.9.6

インストールできました。パスを確認しておきます。

1
2
$ which python
/Users/user/.anyenv/envs/pyenv/shims/python

次にVSCodeでインタプリタの指定を行います。

インタプリタの指定

VSCodeのインタプリタの指定で、インタプリタを選ぶを選択し、先ほど確認したパスのファイルを指定します。

ステータスバー(画面一番下の水色のバー)にPythonのバージョンが表示されるのですが、なぜかPython2.7となっています…

もう一度バージョンとパスを確認します。

1
2
3
4
$ which python    
/Users/user/.anyenv/envs/pyenv/shims/python
$ python --version
Python 3.9.6

ん〜原因がわかりません…

ステータスバーに表示されているPythonのバージョンをクリックすると、インタプリタの選択ができます。

インタープリターの選択と書かれている右に再読み込みボタンがあるので、それをクリックしてみると、先ほどインストールした3.9.6のパスが表示されました。

表示されたバージョンをクリックすると無事3.9.6が選択されたようです。ステータスバーのバージョンも更新されていました。

まとめ

VSCodeは上手に設定しないとその便利さを引き出すことができません。設定したりするのは少し面倒な気がしますが、最初だけなので、しっかりと設定して開発効率を上げていけるようになりたいです。

参考図書