はじめに
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アクセス権限を得るまでの流れは次のようになります。
- 環境変数から
AWS_ROLE_ARN
とAWS_WEB_IDENTITY_TOKEN_FILE
の値を取得する。 - URLパラメータにセットしAWS STS APIサーバへPOSTする。
- レスポンスボディをparseしアクセスキー、シークレットアクセスキー、セッショントークンを取得する。
- s3cmdの設定値に手順3で得たパラメータをセットする。
追記部分のみを以下に記述します。関数role_config
のみ既存のものの書き換えとなります。
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_ARN
、AWS_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
は次のようになります。
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.ts
とk8s/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の設定を入れないといけません。
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を加えないといけません。
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のメリットは大きいと思います。