Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

2024年 研究開発部 新卒開発研修 後編

こんにちは、研究開発部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側で用意されており、適切な要素に値を代入することでリソースが定義されます。

次に、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

© Sansan, Inc.