💡

Terraformでコンテナイメージのビルドからデプロイまでを行う(AWS編)

2024/04/30に公開

概要

  • Terraform の kreuzwerker/docker プロバイダーを使うことで、 terraform apply で行う処理にコンテナイメージのビルドとイメージリポジトリーへの登録の処理も組み込むことができます。
  • この記事ではコンテナイメージを使った AWS Lambda のデプロイをやります。

背景

  • システム構築を行う際、以下のような手順になることがしばしばあります。
    1. コンテナリポジトリーを作る
    2. コンテナイメージの作成とコンテナリポジトリーへの登録 (docker builddocker push)
    3. コンテナリポジトリーに登録したイメージを起動するインフラを作成する。
  • 1 と 3 の処理は terraform で行い、 2 の処理は docker で行う、など各ステップに使用するツールが別になるため、全体のデプロイ処理のために複数ツールを使用するスクリプトを作ることになります。
  • 加えてコンテナリポジトリーへの認証に awscli や docker の認証プラグインなどの準備も必要で、スクリプトを実行できる環境を準備するのは割と手間です。
    • ※あとプロジェクトが Windows / Mac / Linux 混在環境の場合も対応が手間。
  • Terraform の kreuzwerker/docker プロバイダーを使うことで、これを terraform apply で一括して行うことができます。
  • さらに Terraform の実行自体も docker で行うことで、開発者がインストールするのは Docker だけという環境も実現できます。

実装例

https://github.com/ikedam/zenn_snippets/tree/dockerimage_terraform_aws

AWS 上で、コンテナイメージを使う Lambda と API Gateway を通して Hello, World! を返す HTTP エンドポイントを作成するデモです。

こんな感じに docker compose さえ実行できれば、 terraform apply だけで HTTP エンドポイントを構築できます。
※ここでは AWS の認証についての設定等は省略しています。

$ docker compose run --rm terraform init
...
$ docker compose run --rm terraform apply
...
Outputs:

api_url = "https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod"
$ curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod
Hello, World!
$ 

設定のポイント

今回の kreuzwerker/docker の利用での設定のポイントを記載します。

なお、実行のために AWS への認証 (アクセスキーID等の認証情報) をいい感じに取り回す必要がありますが、そこについてはここでは省略します。

TerraformをDockerで起動&TerraformからDockerが使えるようにする

Terraform の Docker イメージを使用することで、プロジェクト参加者が Terraform のインストール手順を踏まなくてもとりあえず Terraform の実行を行えたり、プロジェクトごとに異なる Terraform バージョンを簡単に切り替えて利用できるので便利です。
さらに、今回使用する kreuzwerker/docker プロバイダーは docker デーモンとの通信を行うため、 /var/run/docker.sock をバインドマウントします (docker outside of docker)。

docker-compose.yaml が以下のようになります(関係部分だけ抜粋):

services:
  terraform:
    image: hashicorp/terraform:1.7.5
    environment:
      - AWS_ACCESS_KEY_ID
      - AWS_SECRET_ACCESS_KEY
      - AWS_SESSION_TOKEN
      - AWS_DEFAULT_REGION
    volumes:
      - .:/workspace
      # docker outside of docker でterraformからdockerデーモンにアクセスさせる
      - /var/run/docker.sock:/var/run/docker.sock
    working_dir: /workspace

dockerプロバイダーでECRの認証を行う

AWS ECR 上のイメージリポジトリーにコンテナイメージを保存するには、認証が必要です。
通常、この認証は プライベートレジストリの認証 - Amazon ECR の通り、以下のようなコマンドを実行することで行います。

$ aws ecr get-login-password \
  | docker login \
    --username AWS \
    --password-stdin (AWSアカウントID).dkr.ecr.region.amazonaws.com

これに相当する認証処理を、 以下のように行います :

data "aws_ecr_authorization_token" "token" {
}

provider "docker" {
  registry_auth {
    address  = data.aws_ecr_authorization_token.token.proxy_endpoint
    username = data.aws_ecr_authorization_token.token.user_name
    password = data.aws_ecr_authorization_token.token.password
  }
}

なお、このトークンの値は Terraform のステート(tfstate ファイル)に保存されることに注意してください。このため、以下の運用が必要です:

  • ステートファイルは S3 などのバックエンドに保存するようにして、適切なアクセス制限を行う。
  • Terraform の実行で使用する権限にはIAMロールなどを使用し、個人ユーザーの権限で実行しないようにする。
    • そうでないと tfstate ファイルに保存された個人トークンをプロジェクト内の他のメンバーが使用できてしまう。

docker_imageリソースでコンテナイメージを作成する

docker_image リソース を使うことで、 docker build を実行できます。

扱いが難しい点として、イメージの構築に使用するファイルが使用した際に docker build を再実行するための指定を自前で行う必要があります。

ここでは、ビルドで使用するファイルをarchive_file データソースで zip にして、そのハッシュ値で再ビルドの判定を行うようにしています。この zip ファイルはハッシュ値の計算に使用するだけで、 docker build の処理自体では特に使用しません。

以下のような設定になります :

# イメージのリビルド判定用
data "archive_file" "lambda" {
  type        = "zip"
  output_path = "${path.module}/lambda.zip"
  source_dir  = "${path.module}/lambda"
  # .dockerignore 相当の指定を行う。
  excludes = setunion(
    fileset("${path.module}/lambda", ".devcontainer/**/*"),
    fileset("${path.module}/lambda", ".github/**/*"),
    fileset("${path.module}/lambda", ".gitignore"),
  )
}

resource "docker_image" "lambda" {
  name         = "${aws_ecr_repository.image_repository.repository_url}:latest"
  platform     = "linix/amd64"
  keep_locally = true
  build {
    context = "${path.module}/lambda"
  }
  triggers = {
    sha256 = data.archive_file.lambda.output_sha256
  }
}

なお難点として、ローカルにイメージがない場合、 docker build が実行されてしまいます。
CI/CD などで毎回 docker build が実行される結果になります。

docker_registry_imageでコンテナイメージをpushする

docker_registry_imageリソースdocker push を実行できます。

イメージを更新した場合に docker push を再実行するための指定を自前で行う必要があります。
docker_image リソースと同じ zip のハッシュ値を使用します。

以下のような設定になります :

resource "docker_registry_image" "lambda" {
  name          = docker_image.lambda.name
  keep_remotely = true

  triggers = {
    sha256 = data.archive_file.lambda.output_sha256
  }
}

Lambdaで使用するイメージを指定する

イメージを更新したら新しいイメージがデプロイされるように、
以下のフォーマットのダイジェストを使用したイメージ URL を指定します:

AWSアカウントID.dkr.ecr.リージョン.amazonaws.com/リポジトリー名@sha256:ダイジェスト

ダイジェストの部分は (sha256:ダイジェストのフォーマットで) docker_registry_image リソースの sha256_digest で取得できます:

resource "aws_lambda_function" "lambda" {
  function_name = local.function_name
  role          = aws_iam_role.lambda.arn

  package_type = "Image"
  image_uri    = "${aws_ecr_repository.image_repository.repository_url}@${docker_registry_image.lambda.sha256_digest}"
  publish      = true
}

Discussion