社内コードを公開せずに内部で共有する方法

[Read this article in English.](https://medium.com/@hankehly/how-to-share-private-code-without-exposing-it-to-the-world-9be0b6f0db4e)
# はじめに

見つけやすく、インストールしやすいソフトウェアパッケージは、開発者にとって使いやすいです。[React](https://reactjs.org/)、[Ruby on Rails](https://rubyonrails.org/)、[Airflow](https://qiita.com/hankehly/items/1f02a34740276d1b8f0f) のような有名な OSS は良い事例です。しかし、社内の非公開のコードは、企業秘密として**世間から隠されることが多いです**。権限を持っている人のみ見ることができて、オープンソースのように `npm` `gem` や `pip` で簡単にインストールすることもできません。

その結果、社内のコードがうまく再利用されなくなる（あるいはできなくなる）ことがあります。各チームはそれぞれ独立したコードベースを持ち、他のチームにコードを共有したくても、満足がいく解決策を導き出すことが難しかったりします。戦略を立てないままでは、それぞれの独立したコードベースを充実させ続け「社内共通のライブラリー」が遠い夢のようになっていきます。

あなたの会社でも心当たりがあるのではないでしょうか？**では、社内コードを世界に公開せずに、内部で共有するためにはどうしたらいいのでしょうか？**

この記事では、2つのやり方をおすすめしたいと思います。
1. 共通のコマンドラインツールをコンテナ化し、プライベートレジストリに載せる。
1. 共通のライブラリー(パッケージ)をプライベートリポジトリに載せる。

# 社内コードを公開せずに内部で共有する方法

## 1. コマンドラインツールをコンテナ化する

もし、特定のメンバー（例: 社内の開発者）に共有したいコマンドラインツールがあったら、そのツールをコンテナ化し、プライベートコンテナレジストリに載せることをおすすめします。

### なぜコンテナ化するのか？

Python、Bash、Rubyなどで作った "ちょっとしたスクリプト" は、その場限りで必要とされる場合には、効率がよく素晴らしいものです。しかし、こういったアドホックなスクリプトを何回も使い続けているのであれば、社内の他の人（将来入社する人、退職した後に業務を引き継ぐ人を含む）にもそのニーズがあるでしょう。だったら、みんなの仕事を少しでも楽にできるように、そのスクリプトを可能な限り再利用した方がいいのではないでしょうか？

例えば、`Name` タグを指定して AWS EC2 インスタンスを起動するような、短くて簡単なスクリプトを作ったとします。

```shell
./start_ec2_instances dev-1 dev-2
```

しかし、このままでは、以下のような欠点があります。
* 書いた本人しか使えない
* Aさんの新しいMacBook Proで動かない
* SREチームのAさんが同じことをするスクリプトを3ヶ月前に書いたので、2時間を無駄にしてしまった :sweat_smile:

代わりに、社内にコンテナ化されたスクリプトの一覧を用意しましょう。こうすることで、社内の誰もが、開発言語を問わず貢献できて、他の開発者が作ったスクリプトも使えます。

| イメージURI (READMEへのリンク) | 用途 | 問い合わせ先 |
|:-|:-|:-|
| [ghcr.io/\<company\>/start_ec2_instances](https://example.com) | EC2 インスタンスを `Name` タグで起動する | Aさん (SRE)
| [registry.hub.docker.com/\<company\>/zips3file](https://example.com) | S3へのファイルのダウンロード、zip圧縮、再アップロード | Bさん (Data Science)
| | ... |

実行方法は、オープンソースのコンテナと同じです。

```shell
docker run --rm -v "$HOME/.aws:/root/.aws" ghcr.io/<company>/start_ec2_instances dev-1 dev-2
```

これで、許可さえあれば誰でも実行できて、Linux、Mac M1/Intel、かつWindowsでも挙動が同じです。:open_mouth:

:::note
[buildkit](https://github.com/moby/buildkit/blob/master/docs/multi-platform.md)のようなツールで、コンテナイメージを複数のCPUアーキテクチャー向けにビルドできて、`docker run` 実行時に適切な方を選んでくれます。まるで「どのマシンでも動くバイナリー」のようです。
:::

### どのレジストリを使えばいいのか？

会社がソースコードの管理に一つのGitHubまたはGitLabオーガニゼーションを使う場合は、[GitHub Container Registry](https://docs.github.com/ja/packages/working-with-a-github-packages-registry/working-with-the-container-registry) か [GitLab Container Registry](https://docs.gitlab.com/ee/user/packages/container_registry) をおすすめします。追加料金はかかりませんし、コードと同じプラットフォームで管理できます。

チームごとにオーガニゼーションが分かれていたり、グループ内の他の会社と協力する必要がある場合は、やり方を少し工夫する必要があります。

可能であれば、[Docker Hub](https://hub.docker.com) のようなサービスで「共通コンテナレジストリ」を作成して、全員を招待しましょう。それでも無理なら、[Amazon ECR](https://aws.amazon.com/jp/ecr) のようなクラウドプロバイダーレジストリをおすすめします。[IAMポリシー](https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/repository-policies.html)で他のアカウントのユーザーにコンテナイメージのアクセス権を付与することができて、タグや[コンテキストキー](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_policies_condition-keys.html)で誰にどのコンテナイメージを共有するか制限することもできます。

例えば、以下のIAMポリシーをECRリポジトリに付けると、その中のすべてのコンテナイメージに対して、以下の条件でpush/pullアクセスを許可します。

* リポジトリの `shared` タグが `true` である
* アカウント `123456789012` の IAM ユーザーのみアクセスできる
* `job-category = developer` のタグを持つ IAM ユーザーのみアクセスできる
* 会社の VPN (IP アドレス `123.45.678.90`) に接続されているユーザーのみアクセスできる

```json
{
    "Version": "2012-10-17",
    "Id": "CrossAccountECRPolicy1",
    "Statement": [
        {
            "Sid": "AllowPushPull",
            "Effect": "Allow",
            "Principal": {
                "AWS": [
                    "arn:aws:iam::123456789012:user/*"
                ]
            },
            "Action": [
                "ecr:BatchGetImage",
                "ecr:BatchCheckLayerAvailability",
                "ecr:CompleteLayerUpload",
                "ecr:GetDownloadUrlForLayer",
                "ecr:InitiateLayerUpload",
                "ecr:PutImage",
                "ecr:UploadLayerPart"
            ],
            "Condition": {
                "ecr:ResourceTag/shared": [
                    "true"
                ],
                "StringEquals": {
                    "aws:PrincipalTag/job-category": "developer"
                },
                "IpAddress": {
                    "aws:SourceIp": "123.45.678.90"
                }
            }
        }
    ]
}
```

このIAMポリシーをすべてのECRリポジトリに追加して、共有したいリポジトリだけ `shared` タグを `true` に設定すれば、コンテナイメージのURLを公開しても、↑の条件を満たしたユーザーのみアクセスできます。

### どうやってコンテナ化するのか？

空っぽのディレクトリを作成し、その中にスクリプトと`Dockerfile`を置いてください。

```
start_ec2_instances
├── Dockerfile
├── README.md
├── test123.py
└── requirements.txt
```

:::note
複数の Python スクリプトがある場合、[**click**](https://click.palletsprojects.com) のようなライブラリーで1つのCLIにまとめると、実行しやすくなります。シェルスクリプトで作業している場合、`getopts` や `Makefile` で1つの [ENTRYPOINT](https://docs.docker.jp/engine/reference/builder.html#entrypoint) を用意することもおすすめします。
:::

リモートリポジトリと認証して、イメージをビルドしてプッシュします。[BUILDKIT](https://docs.docker.jp/develop/develop-images/build_enhancements.html)などで複数のCPUアーキテクチャーに対応するのもいいでしょう。
```shell
export DOCKER_BUILDKIT=1
echo $GCR_PAT | docker login ghcr.io -u USERNAME --password-stdin
docker build -t ghcr.io/<github_account_name>/start_ec2_instance --platform linux/amd64,linux/arm64 .
docker push ghcr.io/<github_account_name>/start_ec2_instance
```

これで、コンテナイメージをパブリックリポジトリにアップロードできました。共有したいメンバーに使い方を教えてあげてください。

```shell
docker run --rm ghcr.io/<github_account_name>/start_ec2_instance dev-1 dev-2
```

## 2. 共通のライブラリーをプライベートリポジトリに

もし、特定のメンバーに共有したいソフトウェアパッケージがある場合、[AWS CodeArtifact](https://aws.amazon.com/jp/codeartifact), [Google Cloud Artifact Registry](https://cloud.google.com/artifact-registry) または [GitHub Packages](https://github.co.jp/features/packages) のようなクラウドソリューションで **プライベートリポジトリ** を作ることをおすすめします。パッケージを載せると、アクセス権のある人は `gem`、`npm` や `pip` のような一般的なパッケージマネージャでインストールできます。

:::note
アーティファクトレジストリ/リポジトリとは、バイナリファイルやコードパッケージを保存して配布するための仕組みです。例えば、[PyPI](https://pypi.org) は、PythonのOSSパッケージ用のレジストリです。[npm](https://www.npmjs.com) は、JavaScriptパッケージのレジストリです。
:::

これからは、[AWS CodeArtifact](https://aws.amazon.com/jp/codeartifact)でプライベートリポジトリを作成し、Pythonパッケージを載せ、他のアプリケーションからインストールする流れをお見せします。他の言語でも似たような流れになるはずです。

### AWS CodeArtifact にプライベートPythonパッケージを載せる

1\. CodeArtifactのコンソールを開き、**ドメイン**を作成します。

ドメインは、パッケージをダウンロードする時に使うベースURLのことで、組織/会社ごとに1つあれば十分です。通常は、このURLを指定せずにパッケージをインストールしますが、裏でパブリックリポジトリ（例: [pypi.org](https://pypi.org)）のURLが使われています。
![domains-jp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2525579/cd07f597-672d-efbc-3ff3-686787182666.png)
2\. パッケージを保存するための**リポジトリ**を作成します。前のステップで作ったドメインと関連付けます。
![repositories-jp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2525579/98c0d595-2c78-f465-a4bd-0b0e2eaa7343.png)
3. 新しいディレクトリで、[簡単なPythonパッケージを作成します。](https://packaging.python.org/en/latest/tutorials/packaging-projects/)この記事では、パッケージ名を `test123` とします。
```
test123
├── test123.py
└── pyproject.toml
```
```python:test123.py
def say_hello():
    print("hello world")
```
```toml:pyproject.toml
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "test123"
version = "0.0.1"
requires-python = ">=3.7"
```
4\. AWS CodeArtifactで認証を行います。

パッケージをアップロードする前に、AWSと認証を行う必要があります。認証方法は、ビルドツールによって異なります。
* [Twine](https://twine.readthedocs.io/en/stable/)は、Pythonパッケージを公開するのによく使われるユーティリティで、[AWS CodeArtifactでtwineを使用する方法](https://docs.aws.amazon.com/codeartifact/latest/ug/python-configure-twine.html)を説明する公式ドキュメントがあります。
* [Poetry](https://python-poetry.org)も、Pythonパッケージを作成/公開するためによく使われるツールです。執筆時点では、Poetryに関するAWSドキュメントは存在しませんが、[こちらのブログ記事が参考になります。](https://jasonstitt.com/private-packages-codeartifact-poetry-workflow)

以下のコード例で、↑両方のツールで認証を行う方法を紹介します。

```shell:twine_setup.bash
# 選択肢(1) ログインコマンドを使う
aws codeartifact login --tool twine --domain <my_domain> --repository test123

# 選択肢(2) 環境変数を使う
export TWINE_USERNAME=aws
export TWINE_PASSWORD=`aws codeartifact get-authorization-token --domain <my_domain> --query authorizationToken --output text`
export TWINE_REPOSITORY_URL=`aws codeartifact get-repository-endpoint --domain <my_domain> --repository test123 --format pypi --query repositoryEndpoint --output text`
```
```shell:poetry_setup.bash
# Poetryはログインコマンドがなく、環境変数を使う必要がある
export CODEARTIFACT_AUTH_TOKEN=`aws codeartifact get-authorization-token --domain <my_domain> --query authorizationToken --output text`
# 形式: https://<my_domain>-<account_id>.d.codeartifact.<region>.amazonaws.com/pypi/test123/
export CODEARTIFACT_REPOSITORY_URL=`aws codeartifact get-repository-endpoint --domain <my_domain> --repository test123 --format pypi --query repositoryEndpoint --output text`
export CODEARTIFACT_USER=aws

poetry config repositories.test123 $CODEARTIFACT_REPOSITORY_URL
poetry config http-basic.test123 $CODEARTIFACT_USER $CODEARTIFACT_AUTH_TOKEN
```

:::note warn
macOSでpoetryがBasic HTTP認証に失敗した場合、[このGitHub Issue](https://github.com/python-poetry/poetry/issues/3662#issuecomment-854327667)が参考になります。
:::

5\. 最後に、パッケージをビルドして公開します。
```shell:twine_upload.bash
python3 -m pip install --upgrade build twine
python3 -m build
python3 -m twine upload --repository test123 dist/*
```
```shell:poetry_upload.bash
poetry build
poetry publish -r test123
```

これで、CodeArtifactにパッケージが公開されました。リポジトリの詳細画面を開くと、パッケージが表示されます。URLを同僚に共有しましょう。（必要に応じて、コンテナイメージのようにIAMポリシーでパッケージをインストールできるユーザーを制限できます。）
![repository-ja.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2525579/f7059562-9222-4dde-6671-2cf15c610cea.png)

### プライベートPythonパッケージをインストールする

プライベートPythonパッケージが手に入ったので、Webアプリケーション、Jupyter Notebook、コンテナイメージ、Lambda関数などにインストールできます。

プライベートパッケージをインストールするには、パッケージマネジャーにパッケージのURLを教える必要があります。Pythonの場合は、pip パッケージマネジャーの *index-url* の設定が必要です。方法は、以下の2つがあります。

**1\. (推奨) *index-url* を直接指定する**

`pip` では、`--index-url` パラメータにプライベートリポジトリの URL を指定できます。`poetry` では、`pyproject.toml` の `source` プロパティの設定が必要です。いずれの場合、インストールコマンド実行時にパッケージが存在しなかったら、インストールが失敗します。

**2\. *index-url* の"デフォルト値"としてプライベートリポジトリのURLを設定する**

この場合、パッケージマネジャーは先に公開リポジトリで検索し、パッケージが見つからなかった場合、指定された第2の *index-url* で検索します。`pip` では、[`--extra-index-url`](https://pip.pypa.io/en/stable/cli/pip_install/#cmdoption-extra-index-url) パラメータの指定で設定できます。`poetry` では、`pyproject.toml` の [`secondary`](https://python-poetry.org/docs/repositories/#install-dependencies-from-a-private-repository) フラグを設定します。

:::note warn
悪意を持った人がプライベートパッケージと*同じ名前のパブリックパッケージ*を公開することができます。そのため、`pip` でパッケージをインストールする際には [`--extra-index-url`](https://pip.pypa.io/en/stable/cli/pip_install/#cmdoption-extra-index-url) 引数を、Poetry では [`secondary`](https://python-poetry.org/docs/repositories/#install-dependencies-from-a-private-repository) 属性を使用しないことをおすすめします。リポジトリの検索順番を狙った攻撃は [dependency confusion attack](https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610) と呼ばれます。
:::

**pip を使う場合**

以下のコード例では、AWS CodeArtifact に公開したプライベートパッケージを `pip` でインストールします。

```shell:pip_install.bash
# 1. 認証トークンを取得する
export CODEARTIFACT_AUTH_TOKEN=`aws codeartifact get-authorization-token --domain <my_domain> --query authorizationToken --output text`
# 2. インストール時に --index-url パラメータを指定します。
#    HTTP Basic認証のパスワードには、(1)のトークンを使用します。
python3 -m pip install --index-url "https://aws:${CODEARTIFACT_AUTH_TOKEN}@<my_domain>-<account_id>.d.codeartifact.<region>.amazonaws.com/pypi/test123/simple/" test123==0.0.1
```

:::note
**なぜ URL に `/simple/` を付け足しているのか？**
Pythonパッケージリポジトリには、様々な実装があります。公式な実装（PyPIが使用するもの）は *simple* と呼ばれます。simple API をもとに作られたリポジトリのベースURLは、仕様上 `/simple/` で終わる必要があります。詳しくは [PEP 503](https://peps.python.org/pep-0503/) をご確認ください。
:::

しかし、多くの場合、Pythonアプリケーションの依存パッケージを `requirements.txt` ファイルに記載します。次の例では、`requirements.txt` ファイルに `--index-url` を指定する方法を紹介します。

```:requirements.txt
# PyPI（パブリックリポジトリ）から取得するパッケージを記載するファイル
requests
```
```requirements-private.txt
# プライベートリポジトリから取得するパッケージを記載するファイル
--index-url https://aws:${CODEARTIFACT_AUTH_TOKEN}@<my_domain>-<account_id>.d.codeartifact.<region>.amazonaws.com/pypi/test123/simple/
test123
```
:::note
pipでは、`requirements.txt` 内でPOSIX形式の環境変数（例: `${API_TOKEN}`）が利用できます。
:::

**poetry (<1.2.0) を使う場合**

:::note warn
執筆時点で最新 poetry バージョンが 1.2 ですが、動作確認でプライベートパッケージのインストールが成功せず 1.1.15 をもとに事例紹介させていただいています。1.2 以上での動作確認が出来次第、この記事に追記します。
:::

[poetry](https://python-poetry.org)でパッケージを管理しているアプリケーションに、プライベートパッケージをインストールするには、まず `pyproject.toml` ファイルに `tool.poetry.source` を追加し、リポジトリ名とURLを指定します。

```toml:pyproject.toml
[[tool.poetry.source]]
name = "test123_codeartifact"  # name は CodeArtifact のリポジトリ名と違っても大丈夫
url = "https://<my_domain>-<account_id>.d.codeartifact.<region>.amazonaws.com/pypi/test123/simple/"
```
次に、インストールするプライベートパッケージの `source` プロパティに `tool.poetry.source` ブロックの `name` を設定します。
```toml:pyproject.toml
[tool.poetry.dependencies]
test123 = {version = "^0.0.1", source = "test123_codeartifact"}
requests = "*"
```
Basic HTTP認証でCodeArtifactと認証します。その後、`poetry install` を実行して、すべてのパッケージを同時にインストールします。
```shell:poetry_install.bash
export CODEARTIFACT_AUTH_TOKEN=`aws codeartifact get-authorization-token --domain <my_domain> --query authorizationToken --output text`
export CODEARTIFACT_USER=aws
poetry config http-basic.test123 $CODEARTIFACT_USER $CODEARTIFACT_AUTH_TOKEN
poetry install
```
:::note warn
プライベートパッケージ名/バージョンがPyPI上のパブリックパッケージと重複した場合、インストールコマンドは失敗することがあります。この問題は、[ローカルのpoetryキャッシュを削除することで解決できます。](https://github.com/python-poetry/poetry/issues/4523#issuecomment-1206617779)
:::

### 完全例: コンテナイメージビルド時にプライベートパッケージをインストールする

では、CIなどでコンテナイメージをビルドする時に、どのようにプライベートパッケージをインストールできるのでしょうか？

AWS CodeArtifact から、`get-authorization-token` API経由で認証情報を取得します。その際、`--duration-seconds` で認証情報の有効期限を指定できます。このパラメータを15分などの低い値に設定すると、プライベートパッケージをインストールするための認証情報をそのまま `--build-arg` でコンテナイメージに渡すことができ、レイヤー履歴から隠す必要はありません。

```diff_shell
export CODEARTIFACT_AUTH_TOKEN=`aws codeartifact get-authorization-token \
+   --duration-seconds 900 \
    --domain <my_domain> \
    --query authorizationToken \
    --output text`

docker build \
    --build-arg CODEARTIFACT_USER=aws \
    --build-arg CODEARTIFACT_AUTH_TOKEN=$CODEARTIFACT_AUTH_TOKEN \
    --tag example \
    .
```

**pipを使う場合**

pipを使ったコンテナイメージのビルド事例です。複数のプライベートリポジトリがある場合、リポジトリごとに `requirements-*.txt` を作成するイメージです。

```requirements.txt
requests
```
```requirements-private.txt
--index-url https://aws:${CODEARTIFACT_AUTH_TOKEN}@<my_domain>-<account_id>.d.codeartifact.<region>.amazonaws.com/pypi/test123/simple/
test123
```
```dockerfile:pip_example.Dockerfile
FROM python:3.10.6

ARG CODEARTIFACT_USER
ARG CODEARTIFACT_AUTH_TOKEN

COPY requirements.txt requirements-private.txt .

# requirements-private.txt sets the --index-url to AWS CodeArtifact repository URL
RUN pip install -r requirements.txt \
    && pip install -r requirements-private.txt
```

**poetry (<1.2.0) を使う場合**

```toml:pyproject.toml
[tool.poetry]
name = "app"
version = "0.1.0"
description = ""
authors = ["example <example@example.com>"]

[[tool.poetry.source]]
name = "test123_codeartifact"
url = "https://<my_domain>-<account_id>.d.codeartifact.<region>.amazonaws.com/pypi/test123/simple/"

[tool.poetry.dependencies]
python = "^3.10"
test123 = {version = "*", source = "test123_codeartifact"}
requests = "*"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
```

```dockerfile:poetry_example.Dockerfile
FROM python:3.10.6

ARG CODEARTIFACT_USER
ARG CODEARTIFACT_AUTH_TOKEN
ARG CODEARTIFACT_REPOSITORY_URL

ENV POETRY_VERSION=1.1.15 \
    POETRY_HOME=/usr/local

COPY pyproject.toml poetry.lock .

RUN apt update \
    && apt install -y wget \
    && apt clean \
    && wget https://install.python-poetry.org -O - | python - \
    && poetry config repositories.test123 $CODEARTIFACT_REPOSITORY_URL \
    && poetry config http-basic.test123 ${CODEARTIFACT_USER} ${CODEARTIFACT_AUTH_TOKEN} \
    && poetry install \
    && poetry config --unset http-basic.test
```

# 終わりに

みなさんの会社でも「社内のコードを共有するパターン」を作ってはいかがでしょうか？

今回紹介したような方法で、社内の非公開のコードを**世間に公開することなく**、Lambda関数、Webアプリケーション、Jupyter Notebookなどからインストールできるようになります。開発者同士のコラボレーション/知識共有もしやすくなり、Win-Winですね。

コンテナを起動するには `docker run` を、Pythonパッケージをインストールするには `pip install` を実行するのは、開発者が慣れている作業なので、社内のコードでも同じようにインストールして使えるようにしましょう！

### 参考にした記事

https://jasonstitt.com/private-packages-codeartifact-poetry-workflow

https://dock2learn.com/tech/how-to-specify-non-pypi-requirements-in-your-requirements-txt-file/

### 私と一緒に働きませんか？

ENECHANGE株式会社では、大量な電力時系列データの分析を中心に、様々なアプリケーション企画/開発を行なっています。例えば、こんなことやっています…

* 電気の市場価格によって、家電を自動的に遠隔操作するアプリケーション開発
* 大量の電気消費量データのクラスター分析や需要予測
* [Airflow](https://qiita.com/hankehly/items/1f02a34740276d1b8f0f)を活用したデータパイプライン開発
* AWS Elastic Container Serviceで1,000以上のFargateノードを活用した分散処理

等々。

学習意欲が高く、技術的なチャレンジがほしいPythonエンジニアを募集しています。
ぜひカジュアル面談でお気軽に話しましょう。

https://enechange.co.jp/recruit/requirement/django-engineer/