こんにちは、研究開発部Architectグループの山本です。 前編に引き続き、新卒社員に対して行った研修を紹介したいと思います。 後編では、前編で開発したサービスを社内アプリケーション基盤にデプロイする内容となります。
前編はこちら buildersbox.corp-sansan.com
Docker編
Dockerfileの作成
前回作成した各サービスをDocker環境で動かせるように、Dockerfileを書きます。 実行環境の再現性を向上させたり、後述のアプリケーション基盤(circuit)にデプロイしたりするにはDocker環境の構築が必須となります。 前回作成したサービスにはpythonライブラリ以外に依存するものは特にないため、Dockerfileではpoetryによる環境構築を記述するのがメインとなります。
poetry環境を構築するDockerfileはcookiecutterにより、次のように共通化されています。
FROM python:3.10-slim as python-base # python ENV PYTHONUNBUFFERED=1 \ \ # pip PIP_NO_CACHE_DIR=off \ PIP_DISABLE_PIP_VERSION_CHECK=on \ PIP_DEFAULT_TIMEOUT=100 \ \ # poetry # https://python-poetry.org/docs/configuration/#using-environment-variables POETRY_VERSION=1.6.1 \ # make poetry install to this location POETRY_HOME="/opt/poetry" \ # make poetry create the virtual environment in the project's root # it gets named `.venv` POETRY_VIRTUALENVS_IN_PROJECT=true \ # do not ask any interactive question POETRY_NO_INTERACTION=1 \ \ # paths # this is where our requirements + virtual environment will live PYSETUP_PATH="/opt/pysetup" \ VENV_PATH="/opt/pysetup/.venv" # prepend poetry and venv to path ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:$PATH" # `builder-base` stage is used to build deps + create our virtual environment FROM python-base as builder-base RUN apt-get update \ && apt-get install --no-install-recommends -y \ # deps for installing poetry curl \ # deps for building python deps build-essential # install poetry - respects $POETRY_VERSION & $POETRY_HOME SHELL ["/bin/bash", "-o", "pipefail", "-c"] RUN curl -sSL https://install.python-poetry.org | python - # copy project requirement files here to ensure they will be cached. WORKDIR $PYSETUP_PATH # COPY <コピー元> <コピー元> ... <コピー元> <コピー先> COPY poetry.lock pyproject.toml ./ # install runtime deps RUN poetry install --only main
これに加えて、開発環境用やProduction環境用のDockerコンテナをマルチステージビルドできるように書き加えます。 例えば、APIのDockerfileではDevelopmentとProductionの環境を分けて記述しています。
# `development` image is used during development / testing FROM python-base as development ENV FASTAPI_ENV=development WORKDIR $PYSETUP_PATH # for healthcheck RUN apt-get update && apt-get install --no-install-recommends -y curl # copy in our built poetry + venv COPY --from=builder-base $POETRY_HOME $POETRY_HOME COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH # quicker install as runtime deps are already installed RUN poetry install --with dev # will become mountpoint of our code WORKDIR /app EXPOSE 8000 CMD ["uvicorn", "app.main:app", "--reload", "--host", "0.0.0.0", "--port", "8000"] # `production` image used for runtime FROM python-base as production ENV FASTAPI_ENV=production COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH COPY /app /app/app WORKDIR /app EXPOSE 8000 CMD ["gunicorn", "-c", "app/guniconf.py"]
Developmentのみ、poetry install --with dev
により開発環境向けのpythonライブラリもインストールされます。
pyproject.tomlではdevグループとしてリンタやフォーマッタ、単体テスト関連のライブラリが登録されています。
また、Development環境ではuvicornでAPIを立ち上げていますが、Production環境ではgunicornにより立ち上げられます。
Developmentは、開発環境での実行だけでなく、Github actionsによるCIの実行でも用いられます。
詳しくDockerfileを見たい方は、リポジトリをご覧ください。 github.com
compose.ymlの作成
開発環境で簡単にdockerコンテナを実行できるようにするため、compose.ymlも作成します。 batch, api, appそれぞれのサービスを1つのcompose.ymlにまとめて記述します。
例として、batchでは、Dockerfileのディレクトリ指定だけでなく、マウントするボリュームやenvファイルも指定します。 バッチはAWSやGCPのサービスに依存しているため、ソースコードだけでなく認証情報もマウントされます。
services: api: ... app: ... batch: image: randd/samples/engineering-training/batch:latest build: context: ./batch volumes: - ./batch:/app/ - $HOME/.aws/credentials:/root/.aws/credentials:ro - $HOME/.config/gcloud/application_default_credentials.json:/root/.config/gcloud/application_default_credentials.json:ro env_file: - ./batch/.env
Terraform編
R&Dでは、各サービスのdockerイメージはECRにプッシュされます。 今年度の研修では、TerraformでECRのリポジトリを作成する演習を加えました。
R&Dの組織体制としては、大きく分けて研究員とアーキテクトエンジニアに分かれており、インフラ作成などのタスクは基本的にアーキテクトが行います。 一方で、ECR作成などの定型作業を研究員が行えるようにすると、リードタイムや工数の削減につながります。 さらに、最近になって、PRマージ時にTerraformをクラウドに自動適用するCDが開発されました。 これにより、より効率的なフローでインフラを作成できます。
今回は、研究員、アーキテクトエンジニアに限らずこの作業を実践してもらえるように研修の中に組み込みました。
研修におけるTerraformのディレクトリ構成は以下で
. ├── modules │ ├── ecr │ │ ├── main.tf │ │ └── variables.tf │ └── s3 │ ├── main.tf │ └── variable.tf └── randd-development └── main.tf
modules/では、AWSに適用するTerraformの雛形があります。ここではECRとS3の設定があります。例として、ECRの雛形(modules/ecr/main.tf)は次のようになります。
resource "aws_ecr_repository" "default" { name = var.repository_name } resource "aws_ecr_lifecycle_policy" "default" { repository = aws_ecr_repository.default.name policy = jsonencode( { rules = [ { action = { type = "expire" } description = "DeleteOldImages" rulePriority = 1 selection = { countNumber = 20 countType = "imageCountMoreThan" tagStatus = "any" } }, ] } ) }
TerraformはHCLという言語で書かれています。aws_ecr_repository
でECRそのものを定義しており、aws_ecr_lifecycle_policy
でECRのライフサイクルポリシーを定義しています。aws_ecr_repository
, aws_ecr_lifecycle_policy
の型はTerraform側で用意されており、適切な要素に値を代入することでリソースが定義されます。
aws_ecr_repository
:https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_repositoryaws_ecr_lifecycle_policy
: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_lifecycle_policy
次に、variable.tf
を見てみます。
variable "repository_name" { type = string }
ここでは、この雛形に対して動的に変更したい要素を変数として指定します。引数のようなものだと思ってください。modules/ecr/main.tf
の方で2行目にvar.repository_name
として参照されていることが分かると思います。
modules/
と同じ階層にAWS環境名を示すディレクトリがありますが、ここにこの環境内で作成するリソースを指定します。ここでrandd-development/main.tf
の中身を見てみましょう。
terraform { required_version = "1.7.1" backend "s3" { bucket = "randd-terraform-dev" key = "engineering-training/.tfstate" region = "ap-northeast-1" } } locals { short_stage = "dev" } provider "aws" { region = "ap-northeast-1" } module "s3" { source = "../modules/s3" short_stage = local.short_stage } module "ecr_batch" { source = "../modules/ecr" repository_name = "randd/samples/engineering-training/batch" } module "ecr_api" { source = "../modules/ecr" repository_name = "randd/samples/engineering-training/api" } module "ecr_app" { source = "../modules/ecr" repository_name = "randd/samples/engineering-training/app" }
module
要素では、作成したいリソースを記述します。ここでは、ECRリソースを作成するために、module "ecr_*"
要素を記述しています。source
要素にmodules/ecr/main.tf
への相対パスを入力することで、ECRの設定の雛形を参照しています。また、repository_name
要素では、variables.tf
で定義されている変数の値を入力としています。
実際に編集する箇所は次の通りで、moduleの要素を追加するだけです。
module "ecr_batch_{your_name}" { source = "../modules/ecr" repository_name = "randd/samples/engineering-training/batch-{your_name}" } module "ecr_api_{your_name}" { source = "../modules/ecr" repository_name = "randd/samples/engineering-training/api-{your_name}" } module "ecr_app_{your_name}" { source = "../modules/ecr" repository_name = "randd/samples/engineering-training/app-{your_name}" }
この追加分をPRとして出し、mainブランチにマージされると自動でECRが作られます。
ECRにdocker imageをpushする作業も、github actionsから実行できます。
ブランチ名、AWS環境名、アプリケーション(app, api, batch)、suffix(your_name)を指定しRunすると、githubリポジトリ上のdockerfileから自動でdocker build, pushが実行されます。
Circuit編
最後に、社内のアプリケーション基盤であるCircuit
にデプロイします。
Circuit
については、以下のSpeaker Deckで詳しく解説されています。
speakerdeck.com
Circuitでは、Kubernetesという技術が使われており、マニフェストと呼ばれるインフラコード(YAML)でリソースを定義します。 研修におけるマニフェストファイルのディレクトリ構成は次の通りです。
. ├── api │ ├── base │ │ ├── deployment.yaml │ │ ├── kustomization.yaml │ │ └── service.yaml │ └── overlays │ └── development │ ├── deployment.yaml │ └── kustomization.yaml ├── app │ ├── base │ │ ├── deployment.yaml │ │ ├── ingress.yaml │ │ ├── kustomization.yaml │ │ └── service.yaml │ └── overlays │ └── development │ ├── deployment.yaml │ ├── ingress.yaml │ └── kustomization.yaml ├── batch │ ├── base │ │ ├── cronjob.yaml │ │ └── kustomization.yaml │ └── overlays │ └── development │ ├── cronjob.yaml │ └── kustomization.yaml └── other ├── base │ └── kustomization.yaml └── overlays └── development ├── configmap-bigquery.yaml ├── kustomization.yaml ├── service-account-api.yaml └── service-account-batch.yaml
まず、api, app, batchの各ディレクトリでは、各サービスに関連するリソースを定義しており、othersではサービス共通のリソースやサービスアカウントが定義されています。 上位でbase, overlaysというディレクトリがありますが、baseはすべての環境(Production、Developmentなど)に共通の設定、overlaysでは環境固有の設定が書かれます。 今回はProduction環境にはデプロイしないため、Developmentに対してのみ固有の設定があります。
このように、デプロイするためには多くのマニフェストが書かれていますが、この中でサービス固有の設定は多くありません。 R&Dでは、このマニフェストファイルについてもテンプレートがcookiecutterで定義されており、今回必要なマニフェストのほとんどはcookiecutterにより自動生成されたものになります。
例として、apiのマニフェストに関しては、deployment.yamlとkustomization.yamlにサービス固有の設定が記述されます。
api/base/deployment.yaml
の内容は次の通りです。
apiVersion: apps/v1 kind: Deployment metadata: name: engineering-training-api spec: selector: matchLabels: app: engineering-training-api template: metadata: spec: serviceAccountName: engineering-training containers: - name: engineering-training-api securityContext: runAsNonRoot: true runAsUser: 1000 runAsGroup: 3000 allowPrivilegeEscalation: false env: - name: AWS_DEFAULT_REGION value: "ap-northeast-1" - name: GOOGLE_APPLICATION_CREDENTIALS value: "/var/tmp/config-aws-eks-provider.json" - name: GOOGLE_CLOUD_PROJECT value: "sample-project" volumeMounts: - name: config mountPath: /var/tmp resources: requests: cpu: 500m memory: 1Gi limits: memory: 1Gi volumes: - name: config configMap: name: engineering-training-batch
この中で、修正しなければならないサービス固有の設定はAWS_DEFAULT_REGION
などの環境変数やresources内のcpu, memoryの値です。
他にもengineering-training-*
という値が設定されている箇所があり、こちらもサービス固有のものですが、このような設定はパターンがあり、cookiecutterによるマニフェスト生成時にengineering-training-*
といったリソース名を入力すれば自動で記述されます。
次に、api/overlays/development/deployment.yaml
を見てみましょう。
apiVersion: apps/v1 kind: Deployment metadata: name: engineering-training-api spec: template: spec: containers: - name: engineering-training-api image: "" # イメージを指定 env: - name: BATCH_RESULT_S3_URL_BASE value: s3://sample-bucket/results
こちらで修正すべき箇所は環境変数のみになります。BATCH_RESULT_S3_URL_BASE
という環境変数はDevelopment環境固有の値を保つため、overlays以下に記述されるというわけです。
次に、kustomization.yamlについてです。 kustomization.yamlは、ビルドの最初に読み込まれるマニフェストであり、同じ階層にある他のマニフェストを参照しています。 また、overlays内のkustomization.yamlでは、ECRにあるDocker Imageについての情報も記述されています。
例として、api/overlays/development/kustomization.yamlの内容は次の通りで
resources: - ../../base images: - name: "" # イメージを指定 newTag: "" # タグを指定 patchesStrategicMerge: - deployment.yaml patches: - target: kind: Ingress name: engineering-training-app path: ingress.yaml
ここでは、ECRへのURIと参照するDocker Imageのイメージタグを修正します。 なお、アプリケーション側のソースコードはDocker Imageに集約されているため、今後ソースコードの修正分をCircuitに再デプロイしたい際は、このイメージタグを更新するだけで済むようになります。
app, batchでも同様に修正した後は、マニフェスト用のGithubリポジトリにPRを出します。 これらのマニフェストがmainブランチにマージされれば、自動でCircuitに反映されます。
他のマニフェストについてもリポジトリに公開しています。 興味のある方はぜひご覧ください。 github.com
最後に、今回作成したサービスのCircuit上のインフラ構成は次のようになっています。 ※実際はEKS上でnamespace, cluster, nodeなどが存在しますが、説明のために省略しています。
CircuitはAWSのEKS上に存在します。 Circuitでは、ArgoCDがマニフェストのリポジトリやECRのイメージを参照し、自動で各サービスをデプロイしてくれます。 ユーザがアプリに接続する際は、まずAWS環境のRoute53, Internet Gateway, ELBを通してCircuit内のpodにルーティングされます。 Circuitでは、各サービスのホスト以外に、APIやバッチのサービスアカウントやConfigMapが作成されます。 サービスアカウントはAPI,バッチ用に定義されたもので、S3への編集権限などをもつIAMロールと紐づけられています。 ConfigMapにはPodがGCPのBigQueryにアクセスするための認証情報が定義されています。
まとめ
今回の実践編は、昨年よりもさらにボリューミーとなり、新卒向けとしてはタイトな研修でした。 しかし、普段の業務におけるアプリケーション開発、デプロイの流れが包括的に盛り込まれており、技術的な知識だけでなく社内の運用に関する理解が深まる有意義な内容となったと思います。
今後の発展としては、次のようなものがあります。 - API編でドメイン駆動開発の本質を体験できるようにする 今回開発したAPIは簡易的なもので、ドメイン駆動開発をする必然性はありませんでした。 適切にソースコードの実装を分けることにより、大規模な開発・運用においてどのような利点があるか体験できるような題材にしていきたいです。
- 研修資料とGithubの連携 研修資料を作成するに当たり、参考となるコードをGithubに公開していました。 しかし、Githubのソースコードのみを修正するときなどがあり、研修資料とソースコードの乖離が起きてしまいました。 今後は、研修資料のmdファイルもGithubで管理したり、資料内でのソースコードの参照箇所を、リンクを基にした埋め込みの形式に変えたりするなどして改善していきたいです。
R&Dでは、新卒1年目の社員が次の新卒研修を担当する運用になっています。 教える立場になると、曖昧のまま放置していた知識が体系化され、とても良い学びが得られます。 来年もさらに内容をより良いものにアップデートし、R&Dのすべての新入社員にとって価値のあるものにして欲しいと思います。
最後に、研究開発部ではエンジニア・研究員問わずさまざまな強みをもった方々を募集しています。 もしご興味のある方はぜひご応募ください。 media.sansan-engineering.com