0
0

More than 3 years have passed since last update.

IAM roles for service accountをs3cmdに適用させる

Last updated at Posted at 2020-05-06

はじめに

S3のオブジェクトを管理するためのツールとしてs3cmdがあります。
AWS CLIをインストールせずともS3を操作でき、バックアップやリストアのために使われるケースが多いです。

EKS上でs3cmdを使うにはPodにS3へのアクセス権限が必要となります。
これまではS3へのアクセス権限を与えるために、NodeにIAM Roleを付与したり、kube2iamで一時的にクレデンシャルを取得させたりしていました。
2019年にIAM role for service account(IRSA)が登場し各言語のSDKが対応しましたがs3cmdではSDKを使用していないため、自分で仕組みを実装してみました。

環境

macOS Mojabe 10.14.6
Pulumi 2.1.0
AWS CLI 1.16.292
EKS 1.15
s3cmd 2.1.0

s3cmd改修

s3cmdのソースコードを修正し、DockerイメージをECRにpushしていきます。

s3cmdは下記コマンドでダウンロードしておきます。

$ wget --no-check-certificate https://github.com/s3tools/s3cmd/releases/download/v2.1.0/s3cmd-2.1.0.tar.gz
$ tar xzvf s3cmd-2.1.0.tar.gz
$ cd s3cmd-2.1.0

s3cmd-2.1.0のディレクトリ構造は次のようになっています。

├── INSTALL.md
├── LICENSE
├── MANIFEST.in
├── NEWS
├── PKG-INFO
├── README.md
├── S3/
├── s3cmd
├── s3cmd.1
├── s3cmd.egg-info/
├── setup.cfg
└── setup.py

コード修正

修正するのはS3/Config.pyのみです。S3アクセス権限を得るまでの流れは次のようになります。

  1. 環境変数からAWS_ROLE_ARNAWS_WEB_IDENTITY_TOKEN_FILEの値を取得する。
  2. URLパラメータにセットしAWS STS APIサーバへPOSTする。
  3. レスポンスボディをparseしアクセスキー、シークレットアクセスキー、セッショントークンを取得する。
  4. s3cmdの設定値に手順3で得たパラメータをセットする。

追記部分のみを以下に記述します。関数role_configのみ既存のものの書き換えとなります。

S3/Config.py
import urllib.request
import urllib.parse
import xml.etree.cElementTree

def _get_url():
  stsUrl = "https://sts.amazonaws.com/"
  roleArn = os.environ.get('AWS_ROLE_ARN')
  path = os.environ.get('AWS_WEB_IDENTITY_TOKEN_FILE')
  with open(path) as f:
    webIdentityToken = f.read()
  params = { 
    "Action": "AssumeRoleWithWebIdentity",
    "Version": "2011-06-15",
    "RoleArn": roleArn,
    "RoleSessionName": "s3cmd",
    "WebIdentityToken": webIdentityToken
  }
  url = '{}?{}'.format(stsUrl, urllib.parse.urlencode(params))
  return url

def _build_name_to_xml_node(parent_node):
  if isinstance(parent_node, list):
    return build_name_to_xml_node(parent_node[0])
  xml_dict = {}
  for item in parent_node:
    key = re.compile('{.*}').sub('',item.tag)
    if key in xml_dict:
      if isinstance(xml_dict[key], list):
        xml_dict[key].append(item)
      else:
        xml_dict[key] = [xml_dict[key], item]
    else:
      xml_dict[key] = item
  return xml_dict

def _replace_nodes(parsed):
  for key, value in parsed.items():
    if list(value):
      sub_dict = _build_name_to_xml_node(value)
      parsed[key] = _replace_nodes(sub_dict)
    else:
      parsed[key] = value.text
  return parsed

def _parse_xml_to_dict(body):
  parser = xml.etree.cElementTree.XMLParser(target=xml.etree.cElementTree.TreeBuilder(), encoding='utf-8')
  parser.feed(body)
  root = parser.close()
  parsed = _build_name_to_xml_node(root)
  _replace_nodes(parsed)
  return parsed

class Config(object):
  def role_config(self):
    url = _get_url()
    req = urllib.request.Request(url, method='POST')
    with urllib.request.urlopen(req) as resp:
      body = resp.read()
    parsed = _parse_xml_to_dict(body)

    Config().update_option('access_key', parsed['AssumeRoleWithWebIdentityResult']['Credentials']['AccessKeyId'])
    Config().update_option('secret_key', parsed['AssumeRoleWithWebIdentityResult']['Credentials']['SecretAccessKey'])
    Config().update_option('access_token', parsed['AssumeRoleWithWebIdentityResult']['Credentials']['SessionToken'])

1つずつ見ていきます。


関数_get_urlではSTS APIにPOSTする際のURLを作成するためのものです。
IRSAをPodに適用すると環境変数AWS_ROLE_ARNAWS_WEB_IDENTITY_TOKEN_FILEが作成されます。
後者はファイルパスとなっており中身のトークンを取得してURLパラメータに追加します。

def _get_url():
  stsUrl = "https://sts.amazonaws.com/"
  roleArn = os.environ.get('AWS_ROLE_ARN')
  path = os.environ.get('AWS_WEB_IDENTITY_TOKEN_FILE')
  with open(path) as f:
    webIdentityToken = f.read()
  params = { 
    "Action": "AssumeRoleWithWebIdentity",
    "Version": "2011-06-15",
    "RoleArn": roleArn,
    "RoleSessionName": "s3cmd",
    "WebIdentityToken": webIdentityToken
  }
  url = '{}?{}'.format(stsUrl, urllib.parse.urlencode(params))
  return url



関数_build_name_to_xml_node_replace_nodesはxmlをdictionaryへ変換する処理部分となります。

def _build_name_to_xml_node(parent_node):
  if isinstance(parent_node, list):
    return build_name_to_xml_node(parent_node[0])
  xml_dict = {}
  for item in parent_node:
    key = re.compile('{.*}').sub('',item.tag)
    if key in xml_dict:
      if isinstance(xml_dict[key], list):
        xml_dict[key].append(item)
      else:
        xml_dict[key] = [xml_dict[key], item]
    else:
      xml_dict[key] = item
  return xml_dict

def _replace_nodes(parsed):
  for key, value in parsed.items():
    if list(value):
      sub_dict = _build_name_to_xml_node(value)
      parsed[key] = _replace_nodes(sub_dict)
    else:
      parsed[key] = value.text
  return parsed



関数_parse_xml_to_dictはSTS APIサーバから返ってきたxmlをparserにかけて上述した関数でdictionaryに変換するためのものです。

def _parse_xml_to_dict(body):
  parser = xml.etree.cElementTree.XMLParser(target=xml.etree.cElementTree.TreeBuilder(), encoding='utf-8')
  parser.feed(body)
  root = parser.close()
  parsed = _build_name_to_xml_node(root)
  _replace_nodes(parsed)
  return parsed



関数role_configはs3cmdにIAM Roleを付与する処理として使われます。
dictionaryからアクセスキー、シークレットアクセスキー、セッショントークンを設定します。

class Config(object):
  def role_config(self):
    url = _get_url()
    req = urllib.request.Request(url, method='POST')
    with urllib.request.urlopen(req) as resp:
      body = resp.read()
    parsed = _parse_xml_to_dict(body)

    Config().update_option('access_key', parsed['AssumeRoleWithWebIdentityResult']['Credentials']['AccessKeyId'])
    Config().update_option('secret_key', parsed['AssumeRoleWithWebIdentityResult']['Credentials']['SecretAccessKey'])
    Config().update_option('access_token', parsed['AssumeRoleWithWebIdentityResult']['Credentials']['SessionToken'])

Dockerイメージ作成

コード修正をしたものをs3cmd-2.1.0.tar.gzとして圧縮し、Dockerfileと同じディレクトリに配置します。

├── Dockerfile
└── s3cmd-2.1.0.tar.gz

Dockerfileは次のようになります。

Dockerfile
FROM python:3.8.2-alpine3.11
ARG VERSION=2.1.0
COPY s3cmd-${VERSION}.tar.gz /tmp/
RUN tar -zxf /tmp/s3cmd-${VERSION}.tar.gz -C /tmp && \
    cd /tmp/s3cmd-${VERSION} && \
    python setup.py install && \
    mv s3cmd S3 /usr/local/bin && \
    rm -rf /tmp/*
ENTRYPOINT ["s3cmd"]
CMD ["--help"]

イメージをビルドしてECRにpushします。XXXXXXXXXXXXは自分のAWSアカウントに置き換えてください。

$ docker build -t XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/s3cmd:2.1.0 .
$ docker push XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/s3cmd:2.1.0

デプロイ

今回の環境は全てPulumiで構築します。
ディレクトリ構成は次のようになります。編集するのはindex.tsk8s/s3cmd.yamlのみです。

├── Pulumi.dev.yaml
├── Pulumi.yaml
├── index.ts *
├── k8s
│   └── s3cmd.yaml *
├── node_modules/
├── package-lock.json
├── package.json
├── stack.json
└── tsconfig.json

index.tsでKubernetesのマニフェストファイル以外を記述します。
EKSクラスタはOpenID Connect Providerの設定を入れないといけません。

index.ts
import * as aws from "@pulumi/aws";
import * as awsx from "@pulumi/awsx";
import * as eks from "@pulumi/eks";
import * as k8s from "@pulumi/kubernetes";
import * as pulumi from "@pulumi/pulumi";


const vpc = new awsx.ec2.Vpc("custom", {
  cidrBlock: "10.0.0.0/16",
  numberOfAvailabilityZones: 3,
});

const cluster = new eks.Cluster("pulumi-eks-cluster", {
  vpcId: vpc.id,
  subnetIds: vpc.publicSubnetIds,
  deployDashboard: false,
  createOidcProvider: true,
  instanceType: aws.ec2.T3InstanceSmall,
});

const s3PolicyDocument = pulumi.all([cluster.core.oidcProvider?.arn, cluster.core.oidcProvider?.url]).apply(([arn, url]) => {
  return aws.iam.getPolicyDocument({
    statements: [{
      effect: "Allow",
      principals: [
        {
          type: "Federated",
          identifiers: [arn]
        },
      ],
      actions: ["sts:AssumeRoleWithWebIdentity"],
      conditions: [
        {
          test: "StringEquals",
          variable: url.replace('http://', '') + ":sub",
          values: [
            "system:serviceaccount:default:s3-full-access"
          ]
        },
      ],
    }]
  })
})

const s3FullAccessRole = new aws.iam.Role("s3FullAccessRole", {
  name: "s3-full-access-role",
  assumeRolePolicy: s3PolicyDocument.json,
})

new aws.s3.Bucket("pulumi-s3cmd-test", {
  bucket: "pulumi-s3cmd-test"
});

const s3FullAccessRoleAttachment = new aws.iam.RolePolicyAttachment("s3FullAccessRoleAttachment", {
  role: s3FullAccessRole,
  policyArn: aws.iam.AmazonS3FullAccess,
})

const myk8s = new k8s.Provider("myk8s", {
  kubeconfig: cluster.kubeconfig.apply(JSON.stringify),
});

const s3cmd = new k8s.yaml.ConfigFile("s3cmd", {
  file: "./k8s/s3cmd.yaml"
}, { provider: myk8s })

k8s/s3cmd.yamlではServiceAccountとDeploymentを定義します。
ServiceAccountではannotationsを加えないといけません。

s3cmd.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  namespace: default
  name: s3-full-access
  labels:
    app: s3cmd
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::XXXXXXXXXXXX:role/s3-full-access-role
---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: default
  name: s3cmd
  labels:
    app: s3cmd
spec:
  selector:
    matchLabels:
      app: s3cmd
  replicas: 1
  template:
    metadata:
      labels:
        app: s3cmd
    spec:
      serviceAccountName: s3-full-access
      containers:
      - image: XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/s3cmd:2.1.0
        name: s3cmd
        command: ["/bin/sh"]
        args: ["-c", "while true; do echo hello; sleep 10; done"]

あとは下記コマンドでデプロイするだけです。

$ pulumi up

確認

作成したs3cmdのPodからs3cmdコマンドを打てることを確認します。
今回作成したS3バケットがきちんと表示されます。

$ kubectl get pod
NAME                                                              READY   STATUS    RESTARTS   AGE
s3cmd-98985855f-h5lgl                                             1/1     Running   0          63s

$ kubectl exec -it s3cmd-98985855f-h5lgl -- s3cmd ls
2020-05-02 15:04  s3://pulumi-s3cmd-test

おわりに

kube2iamを使用せず、IRSAでs3cmd PodにIAM Roleを付与できることを確認しました。
kube2iamではDaemonSetをデプロイする必要があり管理リソースが増えてしまうことを考えるとIRSAのメリットは大きいと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0