AWS – Lambda: kubectl

An example of how you can create entities in Kubernetes using AWS Lambda.

The function will be in Python3, so we will use Kubernetes Python Client

More usage examples can be found here.

Since AWS Lambda does not support this package, we will pack the "kubernetes" and "boto3" modules in our function.

"boto3" is needed to access AWS SSM, where kubeconfig will be stored

 

Preparation

Create a directory for the lambda and go to it:

mkdir lambda
cd lambda

 

Next you will need "virtualenv", if it is not there, you can install it using "pip3":

pip3 install virtualenv

 

Create a virtual environment and activate it:

python3 -m virtualenv .
source bin/activate

 

And install the necessary modules:

pip3 install kubernetes
pip3 install boto3

 

Next, we only need the contents of this directory:

$VIRTUAL_ENV/lib/python3.7/site-packages

 

"python3.7" – replace with your version of Python

 

It will be more convenient to look at the environment path and copy the contents to a separately created directory, no longer in a virtual environment:

echo $VIRTUAL_ENV/lib/python3.7/site-packages
/private/tmp/lambda/lib/python3.7/site-packages

 

Let’s create a directory for lambda, which we will already directly download and copy our modules:

mkdir ~/lambda_upload
cp -R /private/tmp/lambda/lib/python3.7/site-packages/. ~/lambda_upload/

 

SSM Parameter Store

In the AWS console, go to "Systems Manager" -> "Parameter Store"

 

And create a parameter with the type "SecureString" and copy the contents of the "kubeconfig" file as the value. If this is an EKS, first create a service account so that it does not require AWS authorization, as in the example below:

Kubeconfig with AWS authorization:

...
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1alpha1
      args:
      - --region
      - eu-central-1
      - eks
      - get-token
      - --cluster-name
      - artem-services-stage-eks
      command: aws

 

Function

Now in the directory "~/lambda_upload" create a file called "lambda_function.py" and paste the following text into it:

lambda_function.py:

import os
import time
import random
import string
import boto3

from kubernetes import config
from kubernetes.client import Configuration
from kubernetes.client.api import core_v1_api
from kubernetes.client.rest import ApiException
from kubernetes.stream import stream

# Get Kubeconfig from SSM
def get_kube_config():
    awsRegion = os.environ['AWS_REGION']
    ssmParameter = os.environ['SSM']

    ssm = boto3.client('ssm', region_name=awsRegion)
    parameter = ssm.get_parameter(Name=ssmParameter, WithDecryption=True)

    kubeconfig = open( '/tmp/kubeconfig.yaml', 'w' )
    kubeconfig.write(parameter['Parameter']['Value'])
    kubeconfig.close()

# Generate random string for unique name of Pod
def randomString(stringLength=8):
    letters = string.ascii_lowercase + string.digits
    return ''.join(random.choice(letters) for i in range(stringLength))

def exec_commands(api_instance):
    name = "busybox-" + randomString()
    namespace = "default"
    resp = None

    print("Creating pod...")

    pod_manifest = {
        'apiVersion': 'v1',
        'kind': 'Pod',
        'metadata': {
            'name': name
        },
        'spec': {
            'containers': [{
                'image': 'busybox',
                'name': 'busybox',
                "args": [
                    "/bin/sh",
                    "-c",
                    "while true;do date;sleep 5; done"
                ]
            }]
        }
    }
    resp = api_instance.create_namespaced_pod(body=pod_manifest, namespace=namespace)

    while True:
        resp = api_instance.read_namespaced_pod(name=name, namespace=namespace)
        if resp.status.phase == 'Pending' or resp.status.phase == 'Running':
            print("Done. Pod " + name + " was created.")
            break
        time.sleep(1)

def main(event, context):
    get_kube_config()

    config.load_kube_config(config_file="/tmp/kubeconfig.yaml")
    c = Configuration()
    c.assert_hostname = False
    Configuration.set_default(c)
    core_v1 = core_v1_api.CoreV1Api()

    exec_commands(core_v1)


if __name__ == '__main__':
    main()

 

Now all the contents of the directory can be packed into a "zip" archive and loaded into the Lambda function.

In a variable environment, create 2 variables in which indicate your AWS Region and the name of the SSM Parameter Store that you created earlier.

 

Lambda functions require read permissions from SSM, so attach the following policy to the role that Lambda uses:

arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess

 

After starting the function, it will create a pod named "busybox-" + the generated string of 8 characters. There is no pod status check, since this script is planned to be used for EKS Fargate, so as not to wait 1-2 minutes until the Fargate instance will running, so in any of the conditions, "Pending" or "Running", we consider that the pod was successful created.

FIX ERROR – AWS Lambda Python: "main() takes 0 positional arguments but 2 were given"

When trying to execute the Lambda Python function, the following error occurs:

{
  "errorMessage": "main() takes 0 positional arguments but 2 were given",
  "errorType": "TypeError",
  "stackTrace": [
    "  File \"/var/runtime/bootstrap.py\", line 131, in handle_event_request\n    response = request_handler(event, lambda_context)\n"
  ]
}

 

Solution:

From the message, we see that we use the "main" function as a handler, which has no incoming arguments.

def main():

 

And to run, you need a handler of 2 arguments "event" and "context", so you need to bring the function declaration to the following form:

def main(event, context):

AWS – Script to get metrics from CloudWatch

Python3 sample script to get metrics from AWS CloudWatch. In the example, we get the maximum value in the last minute and display only the value, this is necessary if you want to collect metrics for example in Zabbix.

 

Script:

#!/usr/bin/env python3

import boto3
import datetime

awsRegion = "eu-west-1" 
namespace = "AWS/ElastiCache"
metric = "CurrConnections"
statistics = "Maximum"
period = 60 # Seconds
timeRange = 1 # Minutes 

client = boto3.client('cloudwatch', region_name=awsRegion)

startTime = (datetime.datetime.utcnow() - datetime.timedelta(minutes=timeRange))
startTime = startTime.strftime("%Y-%m-%dT%H:%M:%S")
endTime = datetime.datetime.utcnow()
endTime = endTime.strftime("%Y-%m-%dT%H:%M:%S")

response = client.get_metric_statistics(
    Namespace=namespace,
    MetricName=metric,
    StartTime=startTime,
    EndTime=endTime,
    Period=period,
    Statistics=[
        statistics,
    ]
)

for cw_metric in response['Datapoints']:
    print(cw_metric['Maximum'])

Jenkins – Active Choice: Harbor – Images tag

 

For a parameterized assembly with an image tag selection, you will need the Active Choices plugin

Go to "Manage Jenkins"

 

Section "Manage Plugins"

 

Go to the "Available" tab and select "Active Choices" in the search.

Install it.

Create a "New Item" – "Pipeline", indicate that it will be a parameterized assembly, and add the parameter "Active Choices Reactive Parameter"

 

We indicate that this is "Groovy Script" and paste the following into it:

import jenkins.model.*
import groovy.json.JsonSlurper

credentialsId = 'harbor_credentials'
harborRepo = 'https://harbor.artem.services/api/repositories/artem/site'

def creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
  com.cloudbees.plugins.credentials.common.StandardUsernameCredentials.class, Jenkins.instance, null, null ).find{
    it.id == credentialsId}

def addr = "${harborRepo}/tags"
def authString = "${creds.username}:${creds.password}".getBytes().encodeBase64().toString()
def conn = addr.toURL().openConnection()
conn.setRequestProperty( "Authorization", "Basic ${authString}" )
def response_json = "${conn.content.text}"
def slurper = new JsonSlurper()
def parsed = slurper.parseText(response_json)

def tags=[]

for (tag in parsed){
    tags.add(tag.name);
}

return tags

 

Where the value of variables, "credentialsId" – Jenkins Credentials ID with login and password to the Harbor;

"harborRepo" – the full path to the desired repository;

 

 

The same thing, but already in Pipeline

Pipeline:

properties([
  parameters([
    [$class: 'CascadeChoiceParameter', 
      choiceType: 'PT_SINGLE_SELECT', 
      description: 'Select Image',
      filterLength: 1,
      filterable: false,
      name: 'ImageTag', 
      script: [
        $class: 'GroovyScript', 
        script: [
          classpath: [], 
          sandbox: false, 
          script: 
            '''
            import jenkins.model.*
            import groovy.json.JsonSlurper

            credentialsId = 'harbor_credentials'
            harborRepo = 'https://harbor.artem.services/api/repositories/artem/site'

            def creds = com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials(
              com.cloudbees.plugins.credentials.common.StandardUsernameCredentials.class, Jenkins.instance, null, null ).find{
                it.id == credentialsId}

            def addr = "${harborRepo}/tags"
            def authString = "${creds.username}:${creds.password}".getBytes().encodeBase64().toString()
            def conn = addr.toURL().openConnection()
            conn.setRequestProperty( "Authorization", "Basic ${authString}" )
            def response_json = "${conn.content.text}"
            def slurper = new JsonSlurper()
            def parsed = slurper.parseText(response_json)

            def tags=[]

            for (tag in parsed){
                tags.add(tag.name);
            }

            return tags
            '''
        ]
      ]
    ]
  ])
])

FIX ERROR – CentOS+Nginx+Jenkins: 502 Bad Gateway

When running Nginx as a reverse proxy to Jenkins on CentOS, a 502 error may occur. Nginx error log will be as follows:

2020/05/07 13:32:33 [crit] 9665#9665: *1 connect() to 127.0.0.1:8080 failed (13: Permission denied) while connecting to upstream, client: 1.2.3.4, server: jenkins.artem.services, request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:8080/", host: "jenkins.artem.services"

 

Solution:

Reason SELinux. You can either allow Jenkins or disable SELinux.

Option to disable SELinux

Open the configuration file:

vim /etc/selinux/config

 

And change "enforcing" to "disabled"

SELINUX=disabled

 

And restart the server.

In CentOS 6, you can disable SELinux without rebooting (CentOS 7/8 – require a reboot) by running the following command:

echo 0 > /selinux/enforce

Linux – Mount partition by label

List of labels

You can view the list of existing labels in the following path:

/dev/disk/by-label/

 

If this directory does not exist, then there are no labels in the system

 

Adding a label

For different file systems, the label is added differently.

ext2/ext3/ext4:

e2label /dev/sda1 LABEL

reiserfs:

reiserfstune -l LABEL /dev/sda1

jfs:

jfs_tune -L LABEL /dev/sda1

xfs:

xfs_admin -L LABEL /dev/sda1

 

Where, "LABEL" – a unique label, "/dev/sda1" – necessary partition

 

fstab

An example of recording in fstab to mount an xfs section on the "SITE" label

LABEL=SITE	/var/www/html/site	xfs	defaults	0 0

 

Jenkins – Active Choice: AWS ECR Images tag (AWS SDK)

 

For a parameterized assembly with an image tag selection, you will need the Active Choices plugin

Go to "Manage Jenkins"

 

Section "Manage Plugins"

 

Go to the "Available" tab and select "Active Choices" in the search.

Install it.

Create a "New Item" – "Pipeline", indicate that it will be a parameterized assembly, and add the parameter "Active Choices Reactive Parameter"

 

We indicate that this is "Groovy Script" and paste the following into it:

import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.ecr.AmazonECR;
import com.amazonaws.services.ecr.AbstractAmazonECR;
import com.amazonaws.services.ecr.AmazonECRClient;
import com.amazonaws.services.ecr.model.ListImagesRequest;
import com.amazonaws.services.ecr.model.ListImagesResult;
import com.amazonaws.services.ecr.AmazonECRClientBuilder;
import com.amazonaws.regions.Region;
import com.amazonaws.regions.RegionUtils;
import com.amazonaws.regions.Regions;
import jenkins.model.*

AmazonECR client = AmazonECRClientBuilder.standard().withRegion("eu-west-1").build();
ListImagesRequest request = new ListImagesRequest().withRepositoryName("artem-services");
res = client.listImages(request);


def result = []
for (image in res) {
   result.add(image.getImageIds());
}

return result[0].imageTag;

 

Where, "eu-west-1" – region where the ECR repository is located;

"artem-services" – the name of your ECR repository;

 

 

The same thing, but already in Pipeline

Pipeline:

properties([
  parameters([
    [$class: 'CascadeChoiceParameter', 
      choiceType: 'PT_SINGLE_SELECT', 
      description: 'Select Image',
      filterLength: 1,
      filterable: false,
      name: 'ImageTag', 
      script: [
        $class: 'GroovyScript', 
        script: [
          classpath: [], 
          sandbox: false, 
          script: 
            '''
            import com.amazonaws.client.builder.AwsClientBuilder;
            import com.amazonaws.services.ecr.AmazonECR;
            import com.amazonaws.services.ecr.AbstractAmazonECR;
            import com.amazonaws.services.ecr.AmazonECRClient;
            import com.amazonaws.services.ecr.model.ListImagesRequest;
            import com.amazonaws.services.ecr.model.ListImagesResult;
            import com.amazonaws.services.ecr.AmazonECRClientBuilder;
            import com.amazonaws.regions.Region;
            import com.amazonaws.regions.RegionUtils;
            import com.amazonaws.regions.Regions;
            import jenkins.model.*

            AmazonECR client = AmazonECRClientBuilder.standard().withRegion("eu-west-1").build();
            ListImagesRequest request = new ListImagesRequest().withRepositoryName("artem-services");
            res = client.listImages(request);


            def result = []
            for (image in res) {
               result.add(image.getImageIds());
            }

            return result[0].imageTag;
            '''
        ]
      ]
    ]
  ])
])

Jenkins – Active Choice: AWS ECR Images tag (AWS Cli)

 

For a parameterized assembly with an image tag selection, you will need the Active Choices plugin

Go to "Manage Jenkins"

 

Section "Manage Plugins"

 

Go to the "Available" tab and select "Active Choices" in the search.

Install it.

Create a "New Item" – "Pipeline", indicate that it will be a parameterized assembly, and add the parameter "Active Choices Reactive Parameter"

 

We indicate that this is "Groovy Script" and paste the following into it:

def command = ['/bin/sh', '-c', '/var/lib/jenkins/.local/bin/aws ecr describe-images --region eu-west-1 --repository-name artem-services --query "sort_by(imageDetails,& imagePushedAt)[ * ].imageTags[ * ]" --output text']
  def proc = command.execute()
  return proc.text.readLines()

 

Where, "/var/lib/jenkins/.local/bin/aws" – is the full path to AWS Cli;

"eu-west-1" – region where the ECR repository is located;

"artem-services" – The name of your ECR repository;

 

 

The same thing, but already in Pipeline

Pipeline:

properties([
  parameters([
    [$class: 'CascadeChoiceParameter', 
      choiceType: 'PT_SINGLE_SELECT', 
      description: 'Select Image',
      filterLength: 1,
      filterable: false,
      name: 'ImageTag', 
      script: [
        $class: 'GroovyScript', 
        script: [
          classpath: [], 
          sandbox: false, 
          script: 
            '''
            def command = ['/bin/sh', '-c', '/var/lib/jenkins/.local/bin/aws ecr describe-images --region eu-west-1 --repository-name artem-services --query "sort_by(imageDetails,& imagePushedAt)[ * ].imageTags[ * ]" --output text']
              def proc = command.execute()
              return proc.text.readLines()
            '''
        ]
      ]
    ]
  ])
])

AWS Cli – ECR display only image tags in the repository

Example using AWS Cli only, without third-party utilities

aws ecr describe-images \
--region eu-west-1 \
--repository-name artem-services \
--query "sort_by(imageDetails,& imagePushedAt)[ * ].imageTags[ * ]" \
--output text

 

Example using "jq" utility

aws ecr describe-images \
--region eu-west-1 \
--repository-name artem-services \
| jq '.imageIds | map (.imageTag)|sort|.[]'