[PYTHON] Appliquer les rôles IAM pour le compte de service à s3cmd

introduction

Il existe s3cmd comme outil de gestion des objets S3. Vous pouvez utiliser S3 sans installer l'AWS CLI, et il est souvent utilisé pour la sauvegarde et la restauration.

Pour utiliser s3cmd sur EKS, Pod doit accéder à S3. Dans le passé, pour donner accès à S3, ** le rôle IAM était attribué à Node ** et ** kube2iam était utilisé pour obtenir temporairement des informations d'identification **. En 2019, rôle IAM pour le compte de service (IRSA) apparaîtra dans chaque langue. Le SDK le prend en charge, mais s3cmd n'utilise pas le SDK, j'ai donc essayé d'implémenter le mécanisme moi-même.

environnement

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

réparation s3cmd

Modifiez le code source de s3cmd et poussez l'image Docker vers ECR.

Téléchargez s3cmd avec la commande suivante.

$ 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

La structure de répertoires de s3cmd-2.1.0 est la suivante.

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

Correction de code

Modifiez uniquement S3 / Config.py. Le flux pour obtenir l'autorisation d'accès S3 est le suivant.

  1. Récupérez les valeurs de ʻAWS_ROLE_ARN et ʻAWS_WEB_IDENTITY_TOKEN_FILE à partir des variables d'environnement.
  2. Définissez le paramètre URL et POST sur le serveur d'API AWS STS.
  3. analysez le corps de la réponse pour obtenir la clé d'accès, la clé d'accès secrète et le jeton de session.
  4. Réglez le paramètre obtenu à l'étape 3 sur la valeur de réglage de s3cmd.

Seule la partie supplémentaire est décrite ci-dessous. Seule la fonction role_config réécrira celle existante.

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'])

Regardons chacun d'eux.
La fonction _get_url sert à créer une URL pour le POST vers l'API STS. L'application d'IRSA à un pod crée des variables d'environnement ʻAWS_ROLE_ARN, ʻAWS_WEB_IDENTITY_TOKEN_FILE. Ce dernier est le chemin du fichier, qui récupère le jeton à l'intérieur et l'ajoute au paramètre 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

Les fonctions `_build_name_to_xml_node` et` _replace_nodes` sont les parties de traitement qui convertissent le XML en dictionnaire.
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

La fonction `_parse_xml_to_dict` sert à convertir le xml renvoyé par le serveur API STS en un dictionnaire avec la fonction ci-dessus en appliquant un analyseur.
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

La fonction `role_config` est utilisée pour attribuer un rôle IAM à s3cmd. Définissez la clé d'accès, la clé d'accès secrète et le jeton de session à partir du dictionnaire.
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'])

Création d'image Docker

Compressez la version modifiée par le code en tant que s3cmd-2.1.0.tar.gz et placez-la dans le même répertoire que Dockerfile.

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

Le Dockerfile ressemble à ceci:

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"]

Construisez l'image et envoyez-la à ECR. Remplacez «XXXXXXXXXXXX» par votre compte 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

Déployer

Tout l'environnement cette fois-ci sera construit avec Pulumi. La structure des répertoires est la suivante. Modifiez uniquement «index.ts» et «k8s / s3cmd.yaml».

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

Écrivez autre chose que le fichier manifeste Kubernetes dans ʻindex.ts`. Le cluster EKS doit inclure les paramètres du fournisseur OpenID Connect.

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 définit ServiceAccount et Deployment. Le compte de service doit ajouter des 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"]

Tout ce que vous avez à faire est de déployer avec la commande suivante.

$ pulumi up

Vérification

Confirmez que vous pouvez taper la commande s3cmd à partir du pod s3cmd créé. Le compartiment S3 créé cette fois s'affiche correctement.

$ 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

en conclusion

J'ai confirmé que le rôle IAM peut être attribué au pod s3cmd par l'IRSA sans utiliser kube2iam. Considérant qu'il est nécessaire de déployer DaemonSet dans kube2iam et que les ressources de gestion vont augmenter, je pense que le mérite d'IRSA est grand.

Recommended Posts

Appliquer les rôles IAM pour le compte de service à s3cmd
Comment utiliser OAuth et API de compte de service avec le client API Google pour python
Mémo de commande pour appliquer la charge pour la vérification des performances