[PYTHON] Wenden Sie IAM-Rollen für das Dienstkonto auf s3cmd an

Einführung

Es gibt s3cmd als Tool zum Verwalten von S3-Objekten. Sie können S3 ohne Installation der AWS CLI betreiben und es wird häufig zum Sichern und Wiederherstellen verwendet.

Um s3cmd unter EKS verwenden zu können, benötigt der Pod Zugriff auf S3. Zuvor erhielt ** Node eine IAM-Rolle **, um Zugriff auf S3 zu gewähren, und ** kube2iam wurde verwendet, um vorübergehend einen Berechtigungsnachweis zu erhalten **. Im Jahr 2019 wird IAM-Rolle für Dienstkonto (IRSA) in jeder Sprache angezeigt. SDK unterstützt es, aber s3cmd verwendet kein SDK, daher habe ich versucht, den Mechanismus selbst zu implementieren.

Umgebung

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

s3cmd reparieren

Ändern Sie den Quellcode von s3cmd und verschieben Sie das Docker-Image auf ECR.

Laden Sie s3cmd mit dem folgenden Befehl herunter.

$ 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

Die Verzeichnisstruktur von s3cmd-2.1.0 ist wie folgt.

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

Code korrigieren

Ändern Sie nur S3 / Config.py. Der Ablauf zum Erhalten der S3-Zugriffsberechtigung ist wie folgt.

  1. Rufen Sie die Werte von AWS_ROLE_ARN und AWS_WEB_IDENTITY_TOKEN_FILE aus den Umgebungsvariablen ab.
  2. Setzen Sie den URL-Parameter und den POST auf den AWS STS-API-Server.
  3. Analysieren Sie den Antworttext, um den Zugriffsschlüssel, den geheimen Zugriffsschlüssel und das Sitzungstoken abzurufen.
  4. Stellen Sie den in Schritt 3 erhaltenen Parameter auf den Einstellwert von s3cmd ein.

Nur der zusätzliche Teil wird unten beschrieben. Nur die Funktion role_config schreibt die vorhandene neu.

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

Schauen wir uns jeden an.
Die Funktion "_get_url" dient zum Erstellen einer URL für das POSTing an die STS-API. Durch Anwenden von IRSA auf einen Pod werden die Umgebungsvariablen "AWS_ROLE_ARN", "AWS_WEB_IDENTITY_TOKEN_FILE" erstellt. Letzteres ist der Dateipfad, über den das Token abgerufen und dem URL-Parameter hinzugefügt wird.

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

Die Funktionen `_build_name_to_xml_node` und` _replace_nodes` sind die Verarbeitungsteile, die XML in ein Wörterbuch konvertieren.
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

Die Funktion "_parse_xml_to_dict" dient zum Konvertieren der vom STS-API-Server zurückgegebenen XML-Datei in ein Wörterbuch mit der obigen Funktion durch Anwenden des Parsers.
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

Die Funktion `role_config` wird verwendet, um s3cmd eine IAM-Rolle zuzuweisen. Legen Sie den Zugriffsschlüssel, den geheimen Zugriffsschlüssel und das Sitzungstoken im Wörterbuch fest.
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-Image-Erstellung

Komprimieren Sie die Code-modifizierte Version als "s3cmd-2.1.0.tar.gz" und legen Sie sie im selben Verzeichnis wie "Dockerfile" ab.

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

Das Dockerfile sieht folgendermaßen aus:

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

Erstellen Sie das Image und senden Sie es an ECR. Ersetzen Sie "XXXXXXXXXXXX" durch Ihr AWS-Konto.

$ 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

Bereitstellen

Die gesamte Umgebung wird diesmal mit Pulumi erstellt. Die Verzeichnisstruktur ist wie folgt. Bearbeiten Sie nur index.ts und k8s / s3cmd.yaml.

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

Beschreiben Sie eine andere als die Kubernetes-Manifestdatei in "index.ts". Der EKS-Cluster muss die OpenID Connect Provider-Einstellungen enthalten.

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 definiert ServiceAccount und Deployment. Das Dienstkonto muss Anmerkungen hinzufügen.

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

Sie müssen lediglich den folgenden Befehl bereitstellen.

$ pulumi up

Bestätigung

Stellen Sie sicher, dass Sie den Befehl s3cmd aus dem erstellten s3cmd-Pod eingeben können. Der diesmal erstellte S3-Bucket wird ordnungsgemäß angezeigt.

$ 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

abschließend

Ich habe bestätigt, dass die IAM-Rolle von IRSA ohne Verwendung von kube2iam dem s3cmd Pod zugewiesen werden kann. In Anbetracht der Tatsache, dass DaemonSet in kube2iam bereitgestellt werden muss und die Verwaltungsressourcen zunehmen werden, denke ich, dass der Nutzen von IRSA großartig ist.

Recommended Posts

Wenden Sie IAM-Rollen für das Dienstkonto auf s3cmd an
Verwendung von OAuth und API für Dienstkonten mit Google API Client für Python
Befehlsnotiz zum Anwenden der Last zur Leistungsüberprüfung