diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..61017f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode +.python-version diff --git a/README.rst b/README.rst index 191b1a0..f1e8618 100644 --- a/README.rst +++ b/README.rst @@ -139,3 +139,47 @@ Celery k8s_worker_image_pull_policy: "{{ k8s_container_image_pull_policy }}" k8s_worker_image_tag: "{{ k8s_container_image_tag }}" k8s_worker_resources: "{{ k8s_container_resources }}" + + +Amazon S3: IAM role for service accounts +```````````````````````````````````````` + +Web applications running on AWS typically use Amazon S3 for static and media +resources. ``caktus.django-k8s`` optionally supports enabling a Kubernetes +service account and associated IAM role that defines the access to public and +private S3 buckets. This provides similar functionality of +`EC2 instance profiles `_ +within Kubernetes namespaces. This +`AWS blog post `_ +also provides a good overview. + +At a high level, the process is: + +1. Create public and private S3 buckets +2. `Enable IAM roles for cluster service accounts `_ + * Requirement: `eksctl `_ must be installed +3. `Create an IAM role with a trust relatinoship and S3 policy for a service account `_ +4. `Annotate the service account with the ARN of the IAM role `_ + +Required variables: + + * ``k8s_s3_cluster_name``: name of EKS cluster in AWS + +A separate playbook can be used to invoke this functionality: + +.. code-block:: yaml + + --- + # file: deploy-s3.yaml + + - hosts: k8s + vars: + ansible_connection: local + ansible_python_interpreter: "{{ ansible_playbook_python }}" + tasks: + - name: configure Amazon S3 buckets + import_role: + name: caktus.django-k8s + tasks_from: aws_s3 + +Run with: ``ansible-playbook deploy-s3.yml``. diff --git a/defaults/main.yml b/defaults/main.yml index 0d09307..4bad9a4 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -144,6 +144,14 @@ k8s_collectstatic_command: - -v - "2" +k8s_s3_cluster_name: "" # name of EKS cluster in AWS +k8s_s3_region: "us-east-1" +k8s_s3_namespace: "{{ k8s_namespace }}" +k8s_s3_iam_role: "S3ServiceAccountRole-{{ k8s_s3_namespace }}" +k8s_s3_serviceaccount: "default" +k8s_s3_public_bucket: "{{ k8s_s3_namespace }}-assets" +k8s_s3_private_bucket: "{{ k8s_s3_namespace }}-private-assets" + k8s_templates: - name: registry_secret.yaml.j2 state: "{{ k8s_dockerconfigjson | ternary('present', 'absent') }}" diff --git a/tasks/aws_s3.yml b/tasks/aws_s3.yml new file mode 100644 index 0000000..b60b18e --- /dev/null +++ b/tasks/aws_s3.yml @@ -0,0 +1,107 @@ +--- + +# +# Public S3 assets bucket +# + +- name: "create public bucket {{ k8s_s3_public_bucket }}" + s3_bucket: + name: "{{ k8s_s3_public_bucket }}" + state: present + versioning: yes + region: "{{ k8s_s3_region }}" + +# +# Private S3 assets bucket +# + +- name: "create private bucket {{ k8s_s3_private_bucket }}" + s3_bucket: + name: "{{ k8s_s3_private_bucket }}" + state: present + versioning: yes + region: "{{ k8s_s3_region }}" + encryption: AES256 + +# Not available via Ansible module as of 7/2020 +- name: "block all public access to {{ k8s_s3_private_bucket }}" + command: + argv: + - aws + - s3api + - put-public-access-block + - --bucket + - "{{ k8s_s3_private_bucket }}" + - --public-access-block-configuration + - BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true + +# +# IAM OIDC identity provider and issuer +# + +# https://docs.aws.amazon.com/eks/latest/userguide/enable-iam-roles-for-service-accounts.html +# Possibly replace in future with (to remove eksctl requirement): +# 1. https://docs.aws.amazon.com/cli/latest/reference/iam/create-open-id-connect-provider.html +# 2. https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html +- name: create an IAM OIDC identity provider for the cluster + command: "eksctl utils associate-iam-oidc-provider --cluster {{ k8s_s3_cluster_name }} --approve" + register: associate_response + changed_when: "'created' in associate_response.stdout" + +# Not available via Ansible module as of 7/2020 +- name: describe cluster to obtain OIDC issuer + command: "aws eks describe-cluster --region {{ k8s_s3_region }} --name {{ k8s_s3_cluster_name }} --output json" + changed_when: false + register: cluster_query + +- name: parse OIDC issuer from response + set_fact: + oidc_issuer: "{{ cluster_query.stdout | from_json | json_query('cluster.identity.oidc.issuer') | regex_replace('https://') }}" + +# +# AWS Account ID +# + +- name: get the current caller identity information + aws_caller_info: + register: caller_info + +- name: parse AWS account ID + set_fact: + aws_account_id: "{{ caller_info.account }}" + +# +# IAM Role +# + +# https://docs.aws.amazon.com/eks/latest/userguide/create-service-account-iam-policy-and-role.html +- name: Create IAM role for K8s service account + iam_role: + name: "{{ k8s_s3_iam_role }}" + assume_role_policy_document: "{{ lookup('template', 's3/TrustPolicy.json.j2') }}" + description: IAM role for K8s service account + +- name: Attach inline policy to user + iam_policy: + iam_type: role + iam_name: "{{ k8s_s3_iam_role }}" + policy_name: "EKSBucketPolicy" + state: present + policy_json: "{{ lookup( 'template', 's3/AssetManagementPolicy.json.j2') }}" + +# +# Service Account +# + +# https://docs.aws.amazon.com/eks/latest/userguide/specify-service-account-role.html +- name: "Associate IAM role with the service account in your cluster" + k8s: + state: present + definition: + apiVersion: v1 + kind: ServiceAccount + metadata: + name: "{{ k8s_s3_serviceaccount }}" + namespace: "{{ k8s_s3_namespace }}" + annotations: + eks.amazonaws.com/role-arn: "arn:aws:iam::{{ aws_account_id }}:role/{{ k8s_s3_iam_role }}" diff --git a/templates/s3/AssetManagementPolicy.json.j2 b/templates/s3/AssetManagementPolicy.json.j2 new file mode 100644 index 0000000..574107b --- /dev/null +++ b/templates/s3/AssetManagementPolicy.json.j2 @@ -0,0 +1,25 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:ListBucket" + ], + "Resource": [ + "arn:aws:s3:::{{ k8s_s3_public_bucket }}", + "arn:aws:s3:::{{ k8s_s3_private_bucket }}" + ] + }, + { + "Effect": "Allow", + "Action": [ + "s3:*" + ], + "Resource": [ + "arn:aws:s3:::{{ k8s_s3_public_bucket }}/*", + "arn:aws:s3:::{{ k8s_s3_private_bucket }}/*" + ] + } + ] +} diff --git a/templates/s3/TrustPolicy.json.j2 b/templates/s3/TrustPolicy.json.j2 new file mode 100644 index 0000000..d819cf2 --- /dev/null +++ b/templates/s3/TrustPolicy.json.j2 @@ -0,0 +1,17 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "arn:aws:iam::{{ aws_account_id }}:oidc-provider/{{ oidc_issuer }}" + }, + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "{{ oidc_issuer }}:sub": "system:serviceaccount:{{ k8s_s3_namespace }}:{{ k8s_s3_serviceaccount }}" + } + } + } + ] +} diff --git a/templates/web.yaml.j2 b/templates/web.yaml.j2 index 5c2ad89..00a616f 100644 --- a/templates/web.yaml.j2 +++ b/templates/web.yaml.j2 @@ -49,7 +49,8 @@ spec: ports: - containerPort: {{ container["port"] }} resources: {{ container["resources"] | to_json }} - securityContext: {} + securityContext: + fsGroup: 2000 --- apiVersion: v1 kind: Service