Security Groups for Podsを試す

今更ながら、Security Groups for Podsを試すメモ。

クラスターの準備

クラスターを作成する。

cat << EOF > cluster.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: sgp-cluster
  region: ap-northeast-1

iam:
  withOIDC: true

managedNodeGroups:
  - name: sample-ng
    instanceType: m5.xlarge
    desiredCapacity: 1
    privateNetworking: true
EOF

作成されたVPCを取得する。

VPCID=$(aws eks describe-cluster --name sgp-cluster \
    --query "cluster.resourcesVpcConfig.vpcId" \
    --output text)

クラスターロールにAmazonEKSVPCResourceControllerポリシーが必要だが、eksctlが勝手につけてくれる。

RDSとセキュリティグループの準備

アプリケーション用のセキュリティグループを作成する。

RDSSG=$(aws ec2 create-security-group --group-name RDSDbAccessSG \
    --description "Security group to apply to apps that need access to RDS" --vpc-id $VPCID \
    --query "GroupId" --output text)

データベース用のセキュリティグループを作成する。

f:id:sotoiwa:20211119150803p:plain

PostgreSQLデータベースを作成する。

f:id:sotoiwa:20211119150823p:plain

f:id:sotoiwa:20211119150840p:plain

f:id:sotoiwa:20211119150855p:plain

f:id:sotoiwa:20211119150910p:plain

IAM認証のためのポリシーを作成する。

DBインスタンスの識別子を確認する。

$ aws rds describe-db-instances --query "DBInstances[*].[DBInstanceIdentifier,DbiResourceId]"

[
    [
        "database-1",
        "db-VPBTU3X2JLLL4CFDAV4JQ2KAGY"
    ],
    [
        "production-database",
        "db-YFON4JI4SDJ53PIDJ43JCQN5DU"
    ],
    [
        "staging-database",
        "db-B7LXVCUYPHCIP4TYSALTKMTVYA"
    ]
]

ポリシーを作成する。

DB_ID="db-VPBTU3X2JLLL4CFDAV4JQ2KAGY"
DB_USER="db_userx"
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --output text --query Account)
AWS_REGION=$(aws configure get region)
cat << EOF > db-connect-policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "rds-db:connect"
            ],
            "Resource": [
                "arn:aws:rds-db:${AWS_REGION}:${AWS_ACCOUNT_ID}:dbuser:${DB_ID}/${DB_USER}"
            ]
        }
    ]
}
EOF
aws iam create-policy \
    --policy-name db-connect-policy \
    --policy-document file://db-connect-policy.json

データベースアカウントを作成する。

Cloud9を同じVPCに立てて作業する。

データベースに接続したら以下を実行。

CREATE USER db_userx;
GRANT rds_iam TO db_userx;

AWS CNIの設定

aws-nodeのバージョンを確認する。

$ kubectl describe daemonset aws-node --namespace kube-system | grep Image | cut -d "/" -f 2
amazon-k8s-cni-init:v1.7.5-eksbuild.1
amazon-k8s-cni:v1.7.5-eksbuild.1

1.8のマニフェストをダウンロードして適用する。

curl -o aws-k8s-cni.yaml https://raw.githubusercontent.com/aws/amazon-vpc-cni-k8s/release-1.8/config/v1.8/aws-k8s-cni.yaml
sed -i.bak -e 's/us-west-2/ap-northeast-1/' aws-k8s-cni.yaml
kubectl apply -f aws-k8s-cni.yaml

1.9のマニフェストをダウンロードして適用する。

curl -o aws-k8s-cni.yaml https://raw.githubusercontent.com/aws/amazon-vpc-cni-k8s/release-1.9/config/v1.9/aws-k8s-cni.yaml
sed -i.bak -e 's/us-west-2/ap-northeast-1/' aws-k8s-cni.yaml
kubectl apply -f aws-k8s-cni.yaml

バージョンを確認する。

$ kubectl describe daemonset aws-node --namespace kube-system | grep Image | cut -d "/" -f 2
amazon-k8s-cni-init:v1.9.3
amazon-k8s-cni:v1.9.3

この段階でENIは2つになっている。

f:id:sotoiwa:20211119150941p:plain

環境変数を設定する。

kubectl set env daemonset -n kube-system aws-node ENABLE_POD_ENI=true

この時点でENIが増えた。

f:id:sotoiwa:20211119150957p:plain

インターフェースのタイプがtrunkになっている(普通はinterface)。

f:id:sotoiwa:20211119151011p:plain

(その後気がついたらaws-k8s-iのほうは消えてENIは2つだけになっていた)

initContainerの環境変数を変更する。

kubectl edit daemonset aws-node -n kube-system

      initContainers:
      - env:
        - name: DISABLE_TCP_EARLY_DEMUX
          value: "true" # falseをtrueに変更

IRSAの設定

IRSAのための設定ファイルを作成する。

AWS_ACCOUNT_ID=$(aws sts get-caller-identity --output text --query Account)
cat << EOF > serviceaccount.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: sgp-cluster
  region: ap-northeast-1

iam:
  withOIDC: true
  serviceAccounts:
  - metadata:
      name: rds-db-access
      namespace: default
      labels: {role: "backend"}
    attachPolicyARNs:
    - "arn:aws:iam::${AWS_ACCOUNT_ID}:policy/db-connect-policy"
EOF

ServiceAccountを作成する。

eksctl create iamserviceaccount --config-file=serviceaccount.yaml --approve

SecurityGroupPolicyの作成

SecurityGroupPolicyに記載するセキュリティグループを確認する。

CLUSTERSG=$(aws eks describe-cluster --name sgp-cluster \
   --query "cluster.resourcesVpcConfig.clusterSecurityGroupId" \
   --output text)
echo $CLUSTERSG $RDSSG
sg-0178264f1bec3ac39 sg-0900d843812be0618

SecurityGroupPolicyの定義ファイルを作成する。セレクターとしてServiceAccountが使われている。

cat << EOF > sgp-policy.yaml
apiVersion: vpcresources.k8s.aws/v1beta1
kind: SecurityGroupPolicy
metadata:
  name: my-sg-policy
spec:
  serviceAccountSelector:
    matchLabels:
      role: backend
  securityGroups:
    groupIds:
      - $CLUSTERSG
      - $RDSSG
EOF

SecurityGroupPolicyを作成する。

$ kubectl apply -f sgp-policy.yaml
securitygrouppolicy.vpcresources.k8s.aws/my-sg-policy created

これでセキュリティグループが増える訳ではなく、新たに作られるPodがセレクターにマッチした場合に指定のセキュリティグループが割り当てられる。

f:id:sotoiwa:20211119151045p:plain

セレクターとしては、PodセレクターとServiceAccountセレクターがある。

$ k explain SecurityGroupPolicy.spec
KIND:     SecurityGroupPolicy
VERSION:  vpcresources.k8s.aws/v1beta1

RESOURCE: spec <Object>

DESCRIPTION:
     SecurityGroupPolicySpec defines the desired state of SecurityGroupPolicy

FIELDS:
   podSelector  <Object>
     A label selector is a label query over a set of resources. The result of
     matchLabels and matchExpressions are ANDed. An empty label selector matches
     all objects. A null label selector matches no objects.

   securityGroups       <Object>
     GroupIds contains the list of security groups that will be applied to the
     network interface of the pod matching the criteria.

   serviceAccountSelector       <Object>
     A label selector is a label query over a set of resources. The result of
     matchLabels and matchExpressions are ANDed. An empty label selector matches
     all objects. A null label selector matches no objects.

イメージのビルド

Pythonのプログラムをpostgres_test_iam.pyとして作成する。

import os

import boto3
import psycopg2

HOST = os.getenv('HOST')
PORT = "5432"
USER = os.getenv('USER')
REGION = "ap-northeast-1"
DBNAME = os.getenv('DATABASE')

session = boto3.Session()
client = boto3.client('rds', region_name=REGION)

token = client.generate_db_auth_token(DBHostname=HOST, Port=PORT, DBUsername=USER, Region=REGION)

conn = None
try:
    conn = psycopg2.connect(host=HOST, port=PORT, database=DBNAME, user=USER, password=token, connect_timeout=3)
    cur = conn.cursor()
    cur.execute("""SELECT version()""")
    query_results = cur.fetchone()
    print(query_results)
    cur.close()
except Exception as e:
    print("Database connection failed due to {}".format(e))
finally:
    if conn is not None:
        conn.close()

Dockerfileを作成する。

FROM python:3.8.5-slim-buster
ADD postgres_test_iam.py /
RUN pip install psycopg2-binary boto3
CMD [ "python", "-u", "./postgres_test_iam.py" ]

イメージをビルドしてECRにプッシュする。

docker build -t postgres-test .
aws ecr create-repository --repository-name postgres-test-demo
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --output text --query Account)
aws ecr get-login-password | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com
docker tag postgres-test ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/postgres-test-demo:latest
docker push ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/postgres-test-demo:latest

デプロイ

アプリをデプロイする。

AWS_ACCOUNT_ID=$(aws sts get-caller-identity --output text --query Account)
DB_HOST="database-1.coxgqjvvcbta.ap-northeast-1.rds.amazonaws.com"
DB_NAME="postgres"
DB_USER="db_userx"
cat << EOF > postgres-test.yaml
apiVersion: v1
kind: Pod
metadata:
  name: postgres-test
spec:
  serviceAccountName: rds-db-access
  containers:
  - name: postgres-test
    image: ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/postgres-test-demo:latest
    env:
    - name: HOST
      value: ${DB_HOST}
    - name: DATABASE
      value: ${DB_NAME}
    - name: USER
      value: ${DB_USER}
EOF
$ kubectl apply -f postgres-test.yaml
pod/postgres-test created

ログを確認する。

$ k get po
NAME            READY   STATUS      RESTARTS   AGE
postgres-test   0/1     Completed   1          16s
$ kubectl logs postgres-test
('PostgreSQL 13.3 on aarch64-unknown-linux-gnu, compiled by gcc (GCC) 7.3.1 20180712 (Red Hat 7.3.1-6), 64-bit',)

ブランチENIが作成されている。

f:id:sotoiwa:20211119151107p:plain

Podを削除する。

$ k delete pod postgres-test
pod "postgres-test" deleted

ブランチENIが消えた。

f:id:sotoiwa:20211119151127p:plain

今度はServiceAccountを指定しないPodを作成する。これによって、ネットワーク的にも、IAM権限的にも繋がらなくなるはず。

AWS_ACCOUNT_ID=$(aws sts get-caller-identity --output text --query Account)
DB_HOST="database-1.coxgqjvvcbta.ap-northeast-1.rds.amazonaws.com"
DB_NAME="postgres"
DB_USER="db_userx"
cat << EOF > postgres-test-no-sa.yaml
apiVersion: v1
kind: Pod
metadata:
  name: postgres-test-no-sa
spec:
  containers:
  - name: postgres-test
    image: ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/postgres-test-demo:latest
    env:
    - name: HOST
      value: ${DB_HOST}
    - name: DATABASE
      value: ${DB_NAME}
    - name: USER
      value: ${DB_USER}
EOF
kubectl apply -f postgres-test-no-sa.yaml

確認する。

$ k get po
NAME                  READY   STATUS      RESTARTS   AGE
postgres-test-no-sa   0/1     Completed   0          5s
$ k logs postgres-test-no-sa
Database connection failed due to connection to server at "database-1.coxgqjvvcbta.ap-northeast-1.rds.amazonaws.com" (192.168.113.177), port 5432 failed: timeout expired

もちろんブランチENIは作成されていない。また、このログはネットワーク的に繋がらなかったエラーであり、generate_db_auth_tokenは成功しているので、このAPIはIAM権限が不要(ローカルで機械的トークンが生成されるだけ?)と思われる。