🔐

AWS FargateからOCI CLIを実行できるようにした

2020/09/24に公開

経緯

OCI ObjectStorageに蓄積されたデータ(具体的には監査ログ)をAthenaで分析するためログファイルをS3に持ってくる必要があり方法を検討しました。

自身の知見や利用できる周辺サービスが多いため、処理を実行するのはAWS側で決定していたのですが、LambdaとFargate、PythonとShellScript、OCI CLIとS3互換APIなど利用技術の選択肢がいくつかありました。
以下のような点から最終的にFargateでOCI CLIとAWS CLIを実行することにしました。

  • Lambdaで使えるストレージが512MBのためログをダウンロードする時の容量に不安があるがFargate(1.4.0)であれば20GB使える。
  • OCI、AWS共に各種SDKではbulk-downloads3 syncのような高レベルコマンドが実行できない。
  • OCI ObjectStorageがS3互換APIを備えているがサポートされるAPIは一部に限られている。

OCI認証情報の扱い

OCI CLIを実行するためにはconfigファイルと秘密鍵が必要です。
SDKおよびCLIの構成ファイル

Dcoker imageの中に認証情報を直接埋め込みたくなかったので、configファイルと秘密鍵はAWS Secrets Managerにバイナリデータとして保存し、Fargateタスク起動時に取得するようにしました。Terraformでバイナリデータのシークレットを作成する場合次のようになります。

# config
resource "aws_secretsmanager_secret" "oci-config" {
  name = "oci-config"
}

resource "aws_secretsmanager_secret_version" "oci-config" {
  secret_id     = aws_secretsmanager_secret.oci-config.id
  secret_binary = filebase64("docker/oci-cli/.oci/config")
}

# 秘密鍵
resource "aws_secretsmanager_secret" "oci-apikey" {
  name = "oci-apikey"
}

resource "aws_secretsmanager_secret_version" "oci-apikey" {
  secret_id     = aws_secretsmanager_secret.oci-apikey.id
  secret_binary = filebase64("docker/oci-cli/.oci/oci_api_key.pem")
}

FargateにはタスクロールでSecrets Managerの権限を渡します。

data "aws_iam_policy_document" "ecs-tasks-assume-role-policy" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "ecs-task-role" {
  name               = "ecs-task-role"
  assume_role_policy = data.aws_iam_policy_document.ecs-tasks-assume-role-policy.json
}

data "aws_iam_policy_document" "ecs-task-secretsmanager" {
  statement {
    effect = "Allow"

    actions = [
      "secretsmanager:GetSecretValue",
    ]

    resources = [
      aws_secretsmanager_secret.oci-config.arn,
      aws_secretsmanager_secret.oci-apikey.arn
    ]
  }
}

resource "aws_iam_policy" "ecs-task-secretsmanager" {
  name   = "ecs-task-secretsmanager"
  policy = data.aws_iam_policy_document.ecs-task-secretsmanager.json
}

resource "aws_iam_role_policy_attachment" "ecs-task-role_ecs-task-secretsmanager" {
  role       = aws_iam_role.ecs-task-role.name
  policy_arn = aws_iam_policy.ecs-task-secretsmanager.arn
}

なおOCI環境上でOCI CLIやSDKを実行する場合はインスタンスプリンシパルというAWSのIAM Roleのような機能で動的に認証情報を渡すこともできます。

そのほかのリソース

ECSとECRの設定をします。

resource "aws_ecr_repository" "oci-cli" {
  name                 = oci-cli
  image_tag_mutability = "MUTABLE"

  image_scanning_configuration {
    scan_on_push = true
  }
}

resource "aws_ecs_cluster" "oci-cli" {
  name = "oci-cli-cluster"

  setting {
    name  = "containerInsights"
    value = "enabled"
  }
}

resource "aws_ecs_task_definition" "oci-cli" {
  family = oci-cli
  container_definitions = templatefile(
    "ecs/task-definition/oci-cli.json",
    {
      region       = var.aws_region
      image        = "${aws_ecr_repository.oci-cli.repository_url}:latest"
      name         = oci-cli
      compartment  = var.compartment
      ociconfig    = aws_secretsmanager_secret.oci-config.name
      ociapikey    = aws_secretsmanager_secret.oci-apikey.name
    }
  )

  execution_role_arn       = "arn:aws:iam::${data.aws_caller_identity.self.account_id}:role/ecsTaskExecutionRole"
  task_role_arn            = aws_iam_role.ecs-task-role.arn
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = 256
  memory                   = 512
}

タスク定義の設定で読み込んでいるテンプレートです。

[
  {
    "logConfiguration": {
      "logDriver": "awslogs",
      "secretOptions": null,
      "options": {
        "awslogs-group": "/ecs/${name}",
        "awslogs-region": "${region}",
        "awslogs-stream-prefix": "ecs"
      }
    },
    "name": "${name}",
    "image": "${image}",
    "essential": true,
    "environment": [
      {
        "name": "COMPARTMENT",
        "value": "${compartment}"
      },
      {
        "name": "OCICONFIG",
        "value": "${ociconfig}"
      },
      {
        "name": "OCIAPIKEY",
        "value": "${ociapikey}"
      }
    ]
  }
]

OCI CLIとAWS CLIをインストールするDockerfileです。一応マルチステージビルドのようなことをやっていますがまだ改良の余地がある気がしています。

# AL2 image Tag
ARG AL2Tag="2.0.20200722.0"

# build
FROM amazonlinux:${AL2Tag} AS builder

COPY requirements.txt /root

RUN yum install -y unzip python3-pip && \
    curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \
    unzip awscliv2.zip && \
    ./aws/install --bin-dir /aws-cli-bin/ && \
    pip3 install -r /root/requirements.txt

# deploy
FROM amazonlinux:${AL2Tag}

COPY --from=builder /usr/local/aws-cli/ /usr/local/aws-cli/
COPY --from=builder /aws-cli-bin/ /usr/local/bin/
COPY --from=builder /root/.cache/pip /root/.cache/pip
COPY --from=builder /root/requirements.txt /root/requirements.txt

RUN yum update -y && \
    yum install -y less groff jq python3-pip && \
    yum clean all && \
    pip3 install -r /root/requirements.txt && \
    rm -rf /root/.cache/pip

RUN mkdir /root/.oci && \
    mkdir /usr/local/oci-cli

COPY oci-cli.sh /usr/local/oci-cli/

ENTRYPOINT ["/usr/local/oci-cli/oci-cli.sh"]

だいぶ簡略化していますが、Fargateで実行するスクリプトはこんな感じです。

#!/bin/bash
# 環境変数からコンパートメントIDやシークレット名を取得
readonly compartment=$COMPARTMENT
readonly ociconfig=$OCICONFIG
readonly ociapikey=$OCIAPIKEY
readonly region="ap-tokyo-1"

# configファイルと秘密鍵の取得
aws secretsmanager get-secret-value \
  --secret-id "$ociconfig" \
  | jq -r .SecretBinary \
  | base64 --decode > /root/.oci/config

aws secretsmanager get-secret-value \
  --secret-id "$ociapikey" \
  | jq -r .SecretBinary \
  | base64 --decode > /root/.oci/oci_api_key.pem

chmod 400 /root/.oci/config
chmod 400 /root/.oci/oci_api_key.pem

# Object Storageのバケット一覧
oci os bucket list \
  --compartment-id "$compartment" \
  --all \
  --region "$region"

実際の環境ではタスクロールにS3の権限を割り当て、 oci os object bulk-download aws s3 sync といったコマンドを実行するスクリプトを配備してObjectStorageからS3へのファイル同期を行なっています。

余談:OCIとS3のバケット名

S3はグローバル(全てのAWSアカウント)で一意な名前をつける必要がありますが、ObjectStorageではOCIアカウント内(実際には割り当てられるネームスペース)のリージョン内で一意であれば良いようです。
オブジェクト・ストレージ・ネームスペースの理解から引用します。

オブジェクト・ストレージ・ネームスペースは、すべてのバケットおよびオブジェクトのコンテナとして機能します。ネームスペース内でバケット名を制御しますが、バケット名は各リージョン内で一意である必要があります。米国西部(フェニックス)にMyBucketという名前のバケットと、ドイツ中央部(フランクフルト)にMyBucketという名前のバケットを持つことができます。

バケット名に関してはOCIの方が柔軟性がありそうでした。

Discussion