<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Kubernetes &#8211; Blog of Kliment Andreev &#8211; A place so I won&#039;t forget things</title>
	<atom:link href="https://blog.andreev.it/category/kubernetes/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.andreev.it</link>
	<description></description>
	<lastBuildDate>Sun, 21 Jan 2024 13:41:42 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	
	<item>
		<title>AWS: Install EKS cluster and Ingress Controller using Terraform and GitLab using OIDC and web identity</title>
		<link>https://blog.andreev.it/2023/11/aws-install-eks-cluster-and-ingress-controller-using-terraform-and-gitlab-using-oidc-and-web-identity/</link>
					<comments>https://blog.andreev.it/2023/11/aws-install-eks-cluster-and-ingress-controller-using-terraform-and-gitlab-using-oidc-and-web-identity/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Tue, 21 Nov 2023 18:39:16 +0000</pubDate>
				<category><![CDATA[AWS]]></category>
		<category><![CDATA[Cloud]]></category>
		<category><![CDATA[Containers]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[EKS]]></category>
		<category><![CDATA[GitLab]]></category>
		<category><![CDATA[ingress]]></category>
		<category><![CDATA[OIDC]]></category>
		<guid isPermaLink="false">https://blog.andreev.it/?p=9581</guid>

					<description><![CDATA[In this post, I&#8217;ll explain how to create an EKS cluster in AWS and&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>In this post, I&#8217;ll explain how to create an EKS cluster in AWS and add an Ingress Controller. We&#8217;ll use GitLab as a repo and execute the CI/CD pipeline from there.<br />
For this you&#8217;ll need:</p>
<ul>
<li>GitLab and AWS account</li>
<li>Terraform, AWS CLI, helm installed</li>
</ul>
<h1>AWS</h1>
<p>From the IAM console, create a new Identity Provider in AWS and use <strong>https://gitlab.com</strong> for both <em>Provider URL</em> and <em>Audience</em>. For self hosted GitLab use <em>gitlab.domain.com</em> or whatever is your GitLab URL.<br />
Click on <strong>Get thumbprint</strong> before clicking on <strong>Add provider</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2023/11/P167-02.png"><img fetchpriority="high" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2023/11/P167-02-1020x1024.png" alt="" width="1020" height="1024" class="aligncenter size-large wp-image-9585" srcset="https://blog.andreev.it/wp-content/uploads/2023/11/P167-02-1020x1024.png 1020w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-02-300x300.png 300w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-02-150x150.png 150w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-02-768x771.png 768w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-02-1170x1175.png 1170w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-02-585x587.png 585w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-02.png 1288w" sizes="(max-width: 1020px) 100vw, 1020px" /></a><br />
In the IAM console, create a role. I&#8217;ll name the role <strong>rolGitLab</strong>, name it whatever you like. When you create the policy, choose <strong>Custom trust policy</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2023/11/P167-04.png"><img decoding="async" src="https://blog.andreev.it/wp-content/uploads/2023/11/P167-04-1024x647.png" alt="" width="1024" height="647" class="aligncenter size-large wp-image-9587" srcset="https://blog.andreev.it/wp-content/uploads/2023/11/P167-04-1024x647.png 1024w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-04-300x190.png 300w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-04-768x485.png 768w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-04-1536x971.png 1536w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-04-1170x739.png 1170w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-04-585x370.png 585w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-04.png 1725w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><br />
Now, here is what you have to decide. If you want to manage the cluster from GitLab only, you can ommit lines 16-23 where I allow the IAM user called gitlab to assume this role. But if you want some IAM user to manage the cluster using aws cli or kubectl, add the ARN of that IAM user in line 20. Add <em>AdministratorAccess </em>to that gitlab user but you can fine-tune those policies later. If you want to manage the cluster from GitLab, you have to register an agent and provide a config file. You can read how to do that <a href="https://docs.gitlab.com/ee/user/clusters/agent/" rel="noopener" target="_blank">here</a>.<br />
Replace the default custom trust policy with this JSON.</p>
<pre class="brush: xml; highlight: [7,12,20]; title: ; notranslate">
{
	&quot;Version&quot;: &quot;2012-10-17&quot;,
	&quot;Statement&quot;: &#x5B;
		{
			&quot;Effect&quot;: &quot;Allow&quot;,
			&quot;Principal&quot;: {
				&quot;Federated&quot;: &quot;arn:aws:iam::123456789012:oidc-provider/gitlab.com&quot;
			},
			&quot;Action&quot;: &quot;sts:AssumeRoleWithWebIdentity&quot;,
			&quot;Condition&quot;: {
				&quot;StringLike&quot;: {
					&quot;gitlab.com:sub&quot;: &quot;project_path:*/eks-gitlab:ref_type:branch:ref:main&quot;
				}
			}
		},
		{
			&quot;Sid&quot;: &quot;&quot;,
			&quot;Effect&quot;: &quot;Allow&quot;,
			&quot;Principal&quot;: {
				&quot;AWS&quot;: &quot;arn:aws:iam::123456789012:user/gitlab&quot;
			},
			&quot;Action&quot;: &quot;sts:AssumeRole&quot;
		}
	]
}
</pre>
<p>In line 7 and 20, change the <em>123456789012 </em>with your account number and in line 7 change <em>gitlab.com</em> with however you named the Identity Provider (IdP). In line 12 replace <em>eks-gitlab </em>with how you plan to name your GitLab project. In line 20 change the <em>gitlab </em>user to however you named your IAM user to access the EKS cluster. Click <strong>Next </strong>and when asked for a permission policy, choose <strong>AdministratorAccess</strong>. Best practice is to limit access to only what&#8217;s needed, but EKS has so many policies, it will take a lot to list them all.<br />
In case you created the gitlab IAM user, attach the AdministratorPolicy (fine tune later) and this inline policy. Change the ARN in line 7. Name this inline policy something like <em>polAssumeGitLabRole</em>. </p>
<pre class="brush: bash; highlight: [7]; title: ; notranslate">
{
    &quot;Version&quot;: &quot;2012-10-17&quot;,
    &quot;Statement&quot;: &#x5B;
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: &quot;sts:AssumeRole&quot;,
            &quot;Resource&quot;: &quot;arn:aws:iam::123456789012:role/rolGitLab&quot;
        }
    ]
}
</pre>
<h1>GitLab</h1>
<p>Create a new blank project in Gitlab with the default settings. In my case the project name is called <strong>eks-gitlab</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2023/11/P167-01.png"><img decoding="async" src="https://blog.andreev.it/wp-content/uploads/2023/11/P167-01-1024x737.png" alt="" width="1024" height="737" class="aligncenter size-large wp-image-9615" srcset="https://blog.andreev.it/wp-content/uploads/2023/11/P167-01-1024x737.png 1024w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-01-300x216.png 300w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-01-768x553.png 768w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-01-1170x843.png 1170w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-01-585x421.png 585w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-01.png 1497w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><br />
Go to <strong>Settings </strong>on the left, then <strong>CI/CD</strong> and then <strong>Variables</strong>. Add these two variables. </p>
<ul>
<li>AWS_PROFILE and a value of <strong>oidc</strong>. Yes, the value is <strong>oidc</strong>. Just type these 4 characters for the value.</li>
<li>ROLE_ARN and a value of the role ARN from the role you just created for <strong>rolGitLab</strong></li>
</ul>
<p>Create a <strong>.gitlab-ci.yml</strong> file with the following content. Change the domain for <em>gitlab.com</em> at line 7 if you use self-hosted GitLab.</p>
<pre class="brush: yaml; highlight: [7]; title: ; notranslate">
assume role:
  image: 
    name: amazon/aws-cli:latest
    entrypoint: &#x5B;&quot;&quot;]
  id_tokens:
    GITLAB_OIDC_TOKEN:
      aud: https://gitlab.com
  before_script:
    - mkdir -p ~/.aws
    - echo &quot;${GITLAB_OIDC_TOKEN}&quot; &gt; /tmp/web_identity_token
    - echo -e &quot;&#x5B;profile oidc]\nrole_arn=${ROLE_ARN}\nweb_identity_token_file=/tmp/web_identity_token&quot; &gt; ~/.aws/config
  script:
      - aws sts get-caller-identity
      - aws s3 ls
</pre>
<p>By the time you commit changes, the pipeline will start and if everything is OK, you&#8217;ll see your S3 buckets in the logs of the job if you go to <strong>Build | Jobs</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2023/11/P167-05.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2023/11/P167-05-878x1024.png" alt="" width="878" height="1024" class="aligncenter size-large wp-image-9617" srcset="https://blog.andreev.it/wp-content/uploads/2023/11/P167-05-878x1024.png 878w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-05-257x300.png 257w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-05-768x895.png 768w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-05-585x682.png 585w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-05.png 1012w" sizes="(max-width: 878px) 100vw, 878px" /></a><br />
One more thing related to Terraform state as we&#8217;ll keep the state in GitLab is that we have to create an <strong>Access Token</strong>. If you have a self-hosted or paid GitLab account, create an <em>Access Token</em> from the <em>Project | Settings | Access Tokens</em>.<br />
If you have a free GitLab account, you have to create a <strong>Personal Access Token</strong> and you do that if you click on your avatar in the upper left corner, then select <strong>Edit Profile</strong> and then click <strong>Access Tokens</strong> on the left. Click <strong>Add new token</strong>, name it <strong>GITLAB_PAT</strong> and select API only. Click <strong>Create personal access token</strong>. Once the token has been created click on the eye icon where it says <em>Your new personal access token</em> and copy &#038; paste somewhere safe.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2023/11/P167-03.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2023/11/P167-03-852x1024.png" alt="" width="852" height="1024" class="aligncenter size-large wp-image-9620" srcset="https://blog.andreev.it/wp-content/uploads/2023/11/P167-03-852x1024.png 852w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-03-250x300.png 250w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-03-768x923.png 768w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-03-585x703.png 585w, https://blog.andreev.it/wp-content/uploads/2023/11/P167-03.png 964w" sizes="(max-width: 852px) 100vw, 852px" /></a><br />
Go to the project settings and add these 2 variables.</p>
<ul>
<li>TF_USERNAME which is your username, click on the avatar, look at the @ under your name, your username is without the @ sign</li>
<li>TF_PASSWORD which is the value of your personal access token you just created</li>
</ul>
<h1>Local machine</h1>
<p>Now that we know the pipeline is working properly, let&#8217;s clone the project on our local machine.<br />
If you plan to manage the cluster from your local machine, now it&#8217;s time to configure the <strong>gitlab </strong>IAM user on your local machine using access keys.<br />
You need to have a valid <a href="https://docs.gitlab.com/ee/user/ssh.html" rel="noopener" target="_blank">key </a>in order to clone your private repo.<br />
Upload your .pub key to GitLab by clicking your avatar, then <strong>Edit Profile</strong>, click <strong>SSH Keys</strong> and then <strong>Add new key</strong>.<br />
Finally, check the connection with the following command. It doesn&#8217;t matter what your local Linux user is, just use git@gitlab.com as the username@hostname. Type as is. </p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
ssh -T git@gitlab.com
Welcome to GitLab, @&#x5B;your_gitlab_username_here]
</pre>
<p>Once you clone the repo add the following files locally.<br />
<strong>FILE: .gitignore</strong></p>
<pre class="brush: bash; collapse: true; light: false; title: ; toolbar: true; notranslate">
# Local .terraform directories
**/.terraform/*

# .tfstate files
*.tfstate
*.tfstate.*

# Crash log files
crash.log
crash.*.log

# Exclude all .tfvars files, which are likely to contain sensitive data, such as
# password, private keys, and other secrets. These should not be part of version 
# control as they are data points which are potentially sensitive and subject 
# to change depending on the environment.
*.tfvars
*.tfvars.json

# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
override.tf.json
*_override.tf
*_override.tf.json

# Include override files you do wish to add to version control using negated pattern
# !example_override.tf

# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*

# Ignore CLI configuration files
.terraformrc
terraform.rc
</pre>
<p><strong>FILE: LICENSE</strong></p>
<pre class="brush: plain; collapse: true; light: false; title: ; toolbar: true; notranslate">
This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.

THE SOFTWARE IS PROVIDED &quot;AS IS&quot;, WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to &lt;https://unlicense.org&gt;
</pre>
<p><strong>FILE: alb_ingress_iam_policy.json</strong></p>
<pre class="brush: xml; collapse: true; light: false; title: ; toolbar: true; notranslate">
{
    &quot;Version&quot;: &quot;2012-10-17&quot;,
    &quot;Statement&quot;: &#x5B;
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: &#x5B;
                &quot;iam:CreateServiceLinkedRole&quot;
            ],
            &quot;Resource&quot;: &quot;*&quot;,
            &quot;Condition&quot;: {
                &quot;StringEquals&quot;: {
                    &quot;iam:AWSServiceName&quot;: &quot;elasticloadbalancing.amazonaws.com&quot;
                }
            }
        },
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: &#x5B;
                &quot;ec2:DescribeAccountAttributes&quot;,
                &quot;ec2:DescribeAddresses&quot;,
                &quot;ec2:DescribeAvailabilityZones&quot;,
                &quot;ec2:DescribeInternetGateways&quot;,
                &quot;ec2:DescribeVpcs&quot;,
                &quot;ec2:DescribeVpcPeeringConnections&quot;,
                &quot;ec2:DescribeSubnets&quot;,
                &quot;ec2:DescribeSecurityGroups&quot;,
                &quot;ec2:DescribeInstances&quot;,
                &quot;ec2:DescribeNetworkInterfaces&quot;,
                &quot;ec2:DescribeTags&quot;,
                &quot;ec2:GetCoipPoolUsage&quot;,
                &quot;ec2:DescribeCoipPools&quot;,
                &quot;elasticloadbalancing:DescribeLoadBalancers&quot;,
                &quot;elasticloadbalancing:DescribeLoadBalancerAttributes&quot;,
                &quot;elasticloadbalancing:DescribeListeners&quot;,
                &quot;elasticloadbalancing:DescribeListenerCertificates&quot;,
                &quot;elasticloadbalancing:DescribeSSLPolicies&quot;,
                &quot;elasticloadbalancing:DescribeRules&quot;,
                &quot;elasticloadbalancing:DescribeTargetGroups&quot;,
                &quot;elasticloadbalancing:DescribeTargetGroupAttributes&quot;,
                &quot;elasticloadbalancing:DescribeTargetHealth&quot;,
                &quot;elasticloadbalancing:DescribeTags&quot;
            ],
            &quot;Resource&quot;: &quot;*&quot;
        },
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: &#x5B;
                &quot;cognito-idp:DescribeUserPoolClient&quot;,
                &quot;acm:ListCertificates&quot;,
                &quot;acm:DescribeCertificate&quot;,
                &quot;iam:ListServerCertificates&quot;,
                &quot;iam:GetServerCertificate&quot;,
                &quot;waf-regional:GetWebACL&quot;,
                &quot;waf-regional:GetWebACLForResource&quot;,
                &quot;waf-regional:AssociateWebACL&quot;,
                &quot;waf-regional:DisassociateWebACL&quot;,
                &quot;wafv2:GetWebACL&quot;,
                &quot;wafv2:GetWebACLForResource&quot;,
                &quot;wafv2:AssociateWebACL&quot;,
                &quot;wafv2:DisassociateWebACL&quot;,
                &quot;shield:GetSubscriptionState&quot;,
                &quot;shield:DescribeProtection&quot;,
                &quot;shield:CreateProtection&quot;,
                &quot;shield:DeleteProtection&quot;
            ],
            &quot;Resource&quot;: &quot;*&quot;
        },
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: &#x5B;
                &quot;ec2:AuthorizeSecurityGroupIngress&quot;,
                &quot;ec2:RevokeSecurityGroupIngress&quot;
            ],
            &quot;Resource&quot;: &quot;*&quot;
        },
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: &#x5B;
                &quot;ec2:CreateSecurityGroup&quot;
            ],
            &quot;Resource&quot;: &quot;*&quot;
        },
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: &#x5B;
                &quot;ec2:CreateTags&quot;
            ],
            &quot;Resource&quot;: &quot;arn:aws:ec2:*:*:security-group/*&quot;,
            &quot;Condition&quot;: {
                &quot;StringEquals&quot;: {
                    &quot;ec2:CreateAction&quot;: &quot;CreateSecurityGroup&quot;
                },
                &quot;Null&quot;: {
                    &quot;aws:RequestTag/elbv2.k8s.aws/cluster&quot;: &quot;false&quot;
                }
            }
        },
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: &#x5B;
                &quot;ec2:CreateTags&quot;,
                &quot;ec2:DeleteTags&quot;
            ],
            &quot;Resource&quot;: &quot;arn:aws:ec2:*:*:security-group/*&quot;,
            &quot;Condition&quot;: {
                &quot;Null&quot;: {
                    &quot;aws:RequestTag/elbv2.k8s.aws/cluster&quot;: &quot;true&quot;,
                    &quot;aws:ResourceTag/elbv2.k8s.aws/cluster&quot;: &quot;false&quot;
                }
            }
        },
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: &#x5B;
                &quot;ec2:AuthorizeSecurityGroupIngress&quot;,
                &quot;ec2:RevokeSecurityGroupIngress&quot;,
                &quot;ec2:DeleteSecurityGroup&quot;
            ],
            &quot;Resource&quot;: &quot;*&quot;,
            &quot;Condition&quot;: {
                &quot;Null&quot;: {
                    &quot;aws:ResourceTag/elbv2.k8s.aws/cluster&quot;: &quot;false&quot;
                }
            }
        },
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: &#x5B;
                &quot;elasticloadbalancing:CreateLoadBalancer&quot;,
                &quot;elasticloadbalancing:CreateTargetGroup&quot;
            ],
            &quot;Resource&quot;: &quot;*&quot;,
            &quot;Condition&quot;: {
                &quot;Null&quot;: {
                    &quot;aws:RequestTag/elbv2.k8s.aws/cluster&quot;: &quot;false&quot;
                }
            }
        },
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: &#x5B;
                &quot;elasticloadbalancing:CreateListener&quot;,
                &quot;elasticloadbalancing:DeleteListener&quot;,
                &quot;elasticloadbalancing:CreateRule&quot;,
                &quot;elasticloadbalancing:DeleteRule&quot;
            ],
            &quot;Resource&quot;: &quot;*&quot;
        },
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: &#x5B;
                &quot;elasticloadbalancing:AddTags&quot;,
                &quot;elasticloadbalancing:RemoveTags&quot;
            ],
            &quot;Resource&quot;: &#x5B;
                &quot;arn:aws:elasticloadbalancing:*:*:targetgroup/*/*&quot;,
                &quot;arn:aws:elasticloadbalancing:*:*:loadbalancer/net/*/*&quot;,
                &quot;arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*&quot;
            ],
            &quot;Condition&quot;: {
                &quot;Null&quot;: {
                    &quot;aws:RequestTag/elbv2.k8s.aws/cluster&quot;: &quot;true&quot;,
                    &quot;aws:ResourceTag/elbv2.k8s.aws/cluster&quot;: &quot;false&quot;
                }
            }
        },
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: &#x5B;
                &quot;elasticloadbalancing:AddTags&quot;,
                &quot;elasticloadbalancing:RemoveTags&quot;
            ],
            &quot;Resource&quot;: &#x5B;
                &quot;arn:aws:elasticloadbalancing:*:*:listener/net/*/*/*&quot;,
                &quot;arn:aws:elasticloadbalancing:*:*:listener/app/*/*/*&quot;,
                &quot;arn:aws:elasticloadbalancing:*:*:listener-rule/net/*/*/*&quot;,
                &quot;arn:aws:elasticloadbalancing:*:*:listener-rule/app/*/*/*&quot;
            ]
        },
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: &#x5B;
                &quot;elasticloadbalancing:ModifyLoadBalancerAttributes&quot;,
                &quot;elasticloadbalancing:SetIpAddressType&quot;,
                &quot;elasticloadbalancing:SetSecurityGroups&quot;,
                &quot;elasticloadbalancing:SetSubnets&quot;,
                &quot;elasticloadbalancing:DeleteLoadBalancer&quot;,
                &quot;elasticloadbalancing:ModifyTargetGroup&quot;,
                &quot;elasticloadbalancing:ModifyTargetGroupAttributes&quot;,
                &quot;elasticloadbalancing:DeleteTargetGroup&quot;
            ],
            &quot;Resource&quot;: &quot;*&quot;,
            &quot;Condition&quot;: {
                &quot;Null&quot;: {
                    &quot;aws:ResourceTag/elbv2.k8s.aws/cluster&quot;: &quot;false&quot;
                }
            }
        },
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: &#x5B;
                &quot;elasticloadbalancing:AddTags&quot;
            ],
            &quot;Resource&quot;: &#x5B;
                &quot;arn:aws:elasticloadbalancing:*:*:targetgroup/*/*&quot;,
                &quot;arn:aws:elasticloadbalancing:*:*:loadbalancer/net/*/*&quot;,
                &quot;arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*&quot;
            ],
            &quot;Condition&quot;: {
                &quot;StringEquals&quot;: {
                    &quot;elasticloadbalancing:CreateAction&quot;: &#x5B;
                        &quot;CreateTargetGroup&quot;,
                        &quot;CreateLoadBalancer&quot;
                    ]
                },
                &quot;Null&quot;: {
                    &quot;aws:RequestTag/elbv2.k8s.aws/cluster&quot;: &quot;false&quot;
                }
            }
        },
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: &#x5B;
                &quot;elasticloadbalancing:RegisterTargets&quot;,
                &quot;elasticloadbalancing:DeregisterTargets&quot;
            ],
            &quot;Resource&quot;: &quot;arn:aws:elasticloadbalancing:*:*:targetgroup/*/*&quot;
        },
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: &#x5B;
                &quot;elasticloadbalancing:SetWebAcl&quot;,
                &quot;elasticloadbalancing:ModifyListener&quot;,
                &quot;elasticloadbalancing:AddListenerCertificates&quot;,
                &quot;elasticloadbalancing:RemoveListenerCertificates&quot;,
                &quot;elasticloadbalancing:ModifyRule&quot;
            ],
            &quot;Resource&quot;: &quot;*&quot;
        }
    ]
}
</pre>
<p><strong>FILE: backend.tf</strong></p>
<pre class="brush: bash; title: ; notranslate">
terraform {
  backend &quot;http&quot; {
  }
}
</pre>
<p><strong>FILE: data.tf</strong></p>
<pre class="brush: bash; title: ; notranslate">
data &quot;aws_availability_zones&quot; &quot;available&quot; {
}

data &quot;aws_eks_cluster&quot; &quot;cluster&quot; {
  name = module.eks.cluster_name
  depends_on = &#x5B;module.eks.cluster_name]
}

data &quot;aws_eks_cluster_auth&quot; &quot;cluster&quot; {
  name = module.eks.cluster_name
  depends_on = &#x5B;module.eks.cluster_name]
}
</pre>
<p><strong>FILE: main.tf NOTE:</strong> Make sure you add lines 40-41 and specify the <strong>rolGitlab</strong> role created earlier. If you don&#8217;t do this when you destroy the cluster, the KMS key for EKS will stay there and there is no way you can delete it unless you call AWS support.</p>
<pre class="brush: bash; collapse: true; light: false; title: ; toolbar: true; notranslate">
# Create a VPC
module &quot;vpc&quot; {
  source  = &quot;terraform-aws-modules/vpc/aws&quot;
  version = &quot;5.0.0&quot;

  name = var.vpcName

  cidr = var.vpcCIDR
  azs  = slice(data.aws_availability_zones.available.names, 0, 3)

  private_subnets = var.privateSubnets
  public_subnets  = var.publicSubnets

  enable_nat_gateway   = var.enable_nat_gateway
  single_nat_gateway   = var.single_nat_gateway
  enable_dns_hostnames = var.enable_dns_hostnames

  public_subnet_tags = {
    &quot;kubernetes.io/cluster/${var.cluster_name}&quot; = &quot;shared&quot;
    &quot;kubernetes.io/role/elb&quot;                    = 1
  }

  private_subnet_tags = {
    &quot;kubernetes.io/cluster/${var.cluster_name}&quot; = &quot;shared&quot;
    &quot;kubernetes.io/role/internal-elb&quot;           = 1
  }
}

# Create an EKS cluster
module &quot;eks&quot; {
  source  = &quot;terraform-aws-modules/eks/aws&quot;
  version = &quot;19.15.3&quot;

  cluster_name    = var.cluster_name
  cluster_version = var.cluster_version

  vpc_id                         = module.vpc.vpc_id
  subnet_ids                     = module.vpc.private_subnets
  cluster_endpoint_public_access = var.cluster_endpoint_public_access
  kms_key_administrators         = &#x5B;&quot;arn:aws:iam::123456789012:role/rolGitLab&quot;]
  kms_key_enable_default_policy  = true

  eks_managed_node_group_defaults = {
    ami_type = var.ami_type
  }

  eks_managed_node_groups = {
    one = {
      name = var.nodeGroup

      instance_types = var.instanceTypes

      min_size     = var.minSize
      max_size     = var.maxSize
      desired_size = var.desiredSize
    }
  }
}

# Create an IAM policy for the ALB ingress
resource &quot;aws_iam_policy&quot; &quot;worker_policy&quot; {
  name        = &quot;worker-policy&quot;
  description = &quot;Worker policy for the ALB Ingress&quot;

  policy = file(var.iamALBIngressPolicyFileName)
}

resource &quot;aws_iam_role_policy_attachment&quot; &quot;additional&quot; {
  for_each = module.eks.eks_managed_node_groups

  policy_arn = aws_iam_policy.worker_policy.arn
  role       = each.value.iam_role_name
}

# Crete the Ingress using Helm chart from AWS
resource &quot;helm_release&quot; &quot;ingress&quot; {
  name       = &quot;ingress&quot;
  chart      = &quot;aws-load-balancer-controller&quot;
  repository = &quot;https://aws.github.io/eks-charts&quot;
  version    = &quot;1.6.1&quot;

  set {
    name  = &quot;autoDiscoverAwsRegion&quot;
    value = &quot;true&quot;
  }

  set {
    name  = &quot;autoDiscoverAwsVpcID&quot;
    value = &quot;true&quot;
  }

  set {
    name  = &quot;clusterName&quot;
    value = module.eks.cluster_name
  }
}
</pre>
<p><strong>FILE: outputs.tf</strong></p>
<pre class="brush: bash; title: ; notranslate">
output &quot;cluster_endpoint&quot; {
  description = &quot;Endpoint for EKS control plane&quot;
  value       = module.eks.cluster_endpoint
}

output &quot;cluster_security_group_id&quot; {
  description = &quot;Security group ids attached to the cluster control plane&quot;
  value       = module.eks.cluster_security_group_id
}

output &quot;region&quot; {
  description = &quot;AWS region&quot;
  value       = var.region
}

output &quot;cluster_name&quot; {
  description = &quot;Kubernetes Cluster Name&quot;
  value       = module.eks.cluster_name
}
</pre>
<p><strong>FILE: providers.tf</strong></p>
<pre class="brush: bash; title: ; notranslate">
provider &quot;aws&quot; {
  region = var.region

  shared_config_files      = &#x5B;&quot;~/.aws/config&quot;]  
  profile                  = &quot;oidc&quot;
}

provider &quot;helm&quot; {
  kubernetes {
    host                   = data.aws_eks_cluster.cluster.endpoint
    token                  = data.aws_eks_cluster_auth.cluster.token
    cluster_ca_certificate = base64decode(data.aws_eks_cluster.cluster.certificate_authority.0.data)
  }
}
</pre>
<p><strong>FILE: variables.tf</strong></p>
<pre class="brush: bash; collapse: true; light: false; title: ; toolbar: true; notranslate">
variable &quot;region&quot; {
  type    = string
  default = &quot;us-east-2&quot;
}

variable &quot;vpcName&quot; {
  type    = string
  default = &quot;vpcFromGitLab&quot;
}

variable &quot;vpcCIDR&quot; {
  type    = string
  default = &quot;10.0.0.0/16&quot;
}

variable &quot;privateSubnets&quot; {
  type    = list(string)
  default = &#x5B;&quot;10.0.1.0/24&quot;, &quot;10.0.2.0/24&quot;, &quot;10.0.3.0/24&quot;]
}

variable &quot;publicSubnets&quot; {
  type    = list(string)
  default = &#x5B;&quot;10.0.4.0/24&quot;, &quot;10.0.5.0/24&quot;, &quot;10.0.6.0/24&quot;]
}

variable &quot;enable_nat_gateway&quot; {
  type    = bool
  default = true
}

variable &quot;single_nat_gateway&quot; {
  type    = bool
  default = false
}

variable &quot;enable_dns_hostnames&quot; {
  type    = bool
  default = true
}

variable &quot;cluster_name&quot; {
  type    = string
  default = &quot;eksFromGitLab&quot;
}

variable &quot;cluster_version&quot; {
  type    = string
  default = &quot;1.27&quot;
}

variable &quot;cluster_endpoint_public_access&quot; {
  type    = bool
  default = true
}

variable &quot;ami_type&quot; {
  type    = string
  default = &quot;AL2_x86_64&quot;
}

variable &quot;nodeGroup&quot; {
  type    = string
  default = &quot;ngFromGitLab&quot;
}

variable &quot;instanceTypes&quot; {
  type    = list(string)
  default = &#x5B;&quot;t3.small&quot;]
}

variable &quot;minSize&quot; {
  type    = number
  default = 1
}

variable &quot;maxSize&quot; {
  type    = number
  default = 3
}

variable &quot;desiredSize&quot; {
  type    = number
  default = 3
}


variable &quot;iamALBIngressPolicyFileName&quot; {
  type    = string
  default = &quot;alb_ingress_iam_policy.json&quot;
}
</pre>
<p>&#8230;and finally your .gitlab-ci.yml file.<br />
<strong>FILE: .gitlab-ci.yml. NOTE: </strong>Remove line 38 if you don&#8217;t use it, don&#8217;t keep it with a comment or you&#8217;ll get an error in the pipeline.</p>
<pre class="brush: bash; highlight: [38]; title: ; notranslate">
stages:          
  - validate
  - plan
  - apply
  - destroy

image:
  name: hashicorp/terraform:1.6
  entrypoint:
    - &quot;/usr/bin/env&quot;
    - &quot;PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin&quot;

variables:
  TF_DIR: ${CI_PROJECT_DIR}
  TF_STATE_NAME: &quot;eks-tf-state&quot;
  TF_ADDRESS: &quot;https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/terraform/state/${TF_STATE_NAME}&quot;

default:
  id_tokens:
    GITLAB_OIDC_TOKEN:
      aud: https://gitlab.com

  before_script:
    - terraform --version
    - cd ${TF_DIR}
    - terraform init
      -reconfigure 
      -backend-config=&quot;address=${TF_ADDRESS}&quot; 
      -backend-config=&quot;lock_address=${TF_ADDRESS}/lock&quot; 
      -backend-config=&quot;unlock_address=${TF_ADDRESS}/lock&quot; 
      -backend-config=&quot;username=${TF_USERNAME}&quot; 
      -backend-config=&quot;password=${TF_PASSWORD}&quot; 
      -backend-config=&quot;lock_method=POST&quot; 
      -backend-config=&quot;unlock_method=DELETE&quot; 
      -backend-config=&quot;retry_wait_min=5&quot;
    - mkdir -p ~/.aws
    - echo &quot;${GITLAB_OIDC_TOKEN}&quot; &gt; /tmp/web_identity_token
    - # echo &quot;${GITLAB_OIDC_TOKEN}&quot; | base64 - If you want to see the token, reveal it this way and then use base64 --decode to get the real one
    - echo -e &quot;&#x5B;profile oidc]\nrole_arn=${ROLE_ARN}\nweb_identity_token_file=/tmp/web_identity_token&quot; &gt; ~/.aws/config

job-validate:
  stage: validate
  script:
    - terraform validate

job-plan:
  stage: plan
  script:
    - terraform plan -out &quot;tf_plan&quot;
  dependencies:
    - job-validate
  artifacts:
    paths:
      - tf_plan

job-apply:
  stage: apply
  script:
    - terraform apply &quot;tf_plan&quot;
  dependencies:
    - job-plan
  allow_failure: true

job-destroy:
  stage: destroy
  script:
    - terraform destroy  -auto-approve
  dependencies:
    #- job-plan
    #- job-apply
  when: manual
</pre>
<p>Push the code back to Gitlab (<em>git add, git commit, git push</em>) and once everything completes successfully, you&#8217;ll have a working EKS cluster after 10-15 mins.<br />
If you choose to manage the EKS from GitLab, you can skip to the next section. If you decide to manage the cluster outside GitLab and you already have another IAM user added to the role <strong>rolGitLab </strong>as described above, then do the following. Replace account # in line 1.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
aws eks update-kubeconfig --region us-east-2 --name eksFromGitLab --role-arn arn:aws:iam::123456789012:role/rolGitLab
aws eks list-clusters
eksctl get cluster --name=eksFromGitLab --region us-east-2
kubectl get svc
</pre>
<h1>Manage EKS from GitLab</h1>
<p>Generally, you would want this to be in another project. Our first project was to build the cluster, but our second project builds on top of that and manages the cluster. In addition, the second project also uses the CI/CD pipeline file <strong>.gitlab-ci.yml</strong> so in order to avoid a single complex project &#8211; just create a new project. I&#8217;ll name my new project &#8211; <strong>project1</strong>. Clone the project on your local machine using <em>git clone</em>.<br />
Create an empty config file for the agent on your local machine. It has to be under this folder structure.</p>
<pre class="brush: bash; title: ; notranslate">
mkdir -p .gitlab/agents/eks-agent &amp;&amp; touch .gitlab/agents/eks-agent/config.yaml
</pre>
<p>In my case the name of the agent is <strong>eks-agent</strong>. Use whatever name you want but follow DNS naming <a href="https://docs.gitlab.com/ee/user/clusters/agent/install/index.html" rel="noopener" target="_blank">convention</a>.<br />
Push the local changes to GitLab.<br />
On the left side in GitLab, go to <strong>Operate | Kubernetes clusters</strong> and then click <strong>Connect a cluster</strong> from the upper right corner.<br />
Select the agent from the drop down and click <strong>Register</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2023/11/P169-02.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2023/11/P169-02-1024x304.png" alt="" width="1024" height="304" class="aligncenter size-large wp-image-9767" srcset="https://blog.andreev.it/wp-content/uploads/2023/11/P169-02-1024x304.png 1024w, https://blog.andreev.it/wp-content/uploads/2023/11/P169-02-300x89.png 300w, https://blog.andreev.it/wp-content/uploads/2023/11/P169-02-768x228.png 768w, https://blog.andreev.it/wp-content/uploads/2023/11/P169-02-1170x347.png 1170w, https://blog.andreev.it/wp-content/uploads/2023/11/P169-02-585x174.png 585w, https://blog.andreev.it/wp-content/uploads/2023/11/P169-02.png 1522w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><br />
You&#8217;ll get an access token and a bunch of helm commands to install the agent on the cluster. Copy and paste and run those commands.</p>
<pre class="brush: bash; title: ; notranslate">
helm repo add gitlab https://charts.gitlab.io
helm repo update
helm upgrade --install eks-agent gitlab/gitlab-agent \
    --namespace gitlab-agent-eks-agent \
    --create-namespace \
    --set image.tag=v16.9.0-rc2 \
    --set config.token=glagent-2r_uW63e1w_mkNzR_keU2hmaNhhcKauSpujg8OdLQG74wrF73g \
    --set config.kasAddress=wss://kas.gitlab.com
</pre>
<p>Refresh the GitLab screen after the helm chart has been installed and you&#8217;ll see your cluster connected.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2023/11/P169-03.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2023/11/P169-03-1024x174.png" alt="" width="1024" height="174" class="aligncenter size-large wp-image-9769" srcset="https://blog.andreev.it/wp-content/uploads/2023/11/P169-03-1024x174.png 1024w, https://blog.andreev.it/wp-content/uploads/2023/11/P169-03-300x51.png 300w, https://blog.andreev.it/wp-content/uploads/2023/11/P169-03-768x131.png 768w, https://blog.andreev.it/wp-content/uploads/2023/11/P169-03-1536x261.png 1536w, https://blog.andreev.it/wp-content/uploads/2023/11/P169-03-2048x348.png 2048w, https://blog.andreev.it/wp-content/uploads/2023/11/P169-03-1920x326.png 1920w, https://blog.andreev.it/wp-content/uploads/2023/11/P169-03-1170x199.png 1170w, https://blog.andreev.it/wp-content/uploads/2023/11/P169-03-585x99.png 585w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><br />
Now you need to <a href="https://docs.gitlab.com/ee/user/clusters/agent/ci_cd_workflow.html#authorize-the-agent" rel="noopener" target="_blank">authorize</a> the agent to access our project.<br />
In the blank <strong>config.yaml</strong> file on the local machine under <em>.gitlab/agents/eks-agent/</em> directory, add these lines. Change the path the project in line 3 (look at the URL, that&#8217;s your project name after gitlab.com) and change the manifest file if you want in line 6. You can also change the namespace in line 5 as well.</p>
<pre class="brush: yaml; highlight: [3,6]; title: ; notranslate">
gitops:
  manifest_projects:
  - id: andreevkliment/project1
    default_namespace: default
    paths:
      - glob: &#039;manifest.yaml&#039;
</pre>
<p>Then create the manifest file under the root of the project. Save it as <em>manifest.yaml</em> or however you referenced it in line 6 in the previous file.</p>
<pre class="brush: yaml; title: ; notranslate">
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
</pre>
<p>If you push this code back to GitLab, the agent will execute it and you&#8217;ll (with <em>kubect get pods</em>) see the nginx pods running on your cluster.<br />
Another way of managing the EKS cluster is using <strong>.gitlab-ci.yml</strong> file.<br />
Create <strong>.gitlab-ci.yml</strong> with these lines. Replace your username, project and the agent name in line 7.</p>
<pre class="brush: yaml; highlight: [7]; title: ; notranslate">
deploy:
  image:
    name: bitnami/kubectl:latest
    entrypoint: &#x5B;&#039;&#039;]
  script:
    - kubectl config get-contexts
    - kubectl config use-context andreevkliment/project1:eks-agent
    - kubectl get pods
</pre>
<p>If you push this back to GitLab, the pipeline will start and you&#8217;ll see the pods from the cluster in the logs. This example doesn&#8217;t execute <strong>manifest.yaml</strong>. You can have both ways of interacting with the cluster or you can use them separately.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2023/11/aws-install-eks-cluster-and-ingress-controller-using-terraform-and-gitlab-using-oidc-and-web-identity/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Install MetalLB on Kubernetes cluster running on VMWare VMs or bare metal server and nginx ingress</title>
		<link>https://blog.andreev.it/2023/10/install-metallb-on-kubernetes-cluster-running-on-vmware-vms-or-bare-metal-server/</link>
					<comments>https://blog.andreev.it/2023/10/install-metallb-on-kubernetes-cluster-running-on-vmware-vms-or-bare-metal-server/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Sat, 21 Oct 2023 23:05:18 +0000</pubDate>
				<category><![CDATA[DevOps]]></category>
		<category><![CDATA[ESX/ESXi]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[bare metal]]></category>
		<category><![CDATA[certificate]]></category>
		<category><![CDATA[ESXi]]></category>
		<category><![CDATA[ingress]]></category>
		<category><![CDATA[load balancer]]></category>
		<category><![CDATA[MetalLB]]></category>
		<category><![CDATA[nginx]]></category>
		<category><![CDATA[ssl]]></category>
		<category><![CDATA[TLS]]></category>
		<category><![CDATA[VMware]]></category>
		<guid isPermaLink="false">https://blog.andreev.it/?p=9551</guid>

					<description><![CDATA[In this post I&#8217;ll explain how to install MetalLB which is a load balancer&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>In this post I&#8217;ll explain how to install <a href="https://metallb.universe.tf/" rel="noopener" target="_blank">MetalLB </a>which is a load balancer for bare metal/VM Kubernetes servers. If you have your own home lab to play with Kubernetes and you are not using AKS, EKS or GKE, then this post is for you.<br />
The prerequisite is to have a running Kubernetes cluster. In my earlier <a href="https://blog.andreev.it/2023/10/install-kubernetes-with-cri-o-and-cilium-on-rocky-linux-9/" rel="noopener" target="_blank">post</a>, I&#8217;ve described how to install Kubernetes with CRI-O and Cilium as a CNI, so you can follow that post or feel free to have your own install. Mind that if you use other CNIs such as Calico or Weave, there are some things you have to check first and see if it applies to you. Look at this <a href="https://metallb.universe.tf/configuration/" rel="noopener" target="_blank">link</a>.<br />
We&#8217;ll install MetalLB now. You probably have your k8s master and the nodes ready.</p>
<h1>Install MetalLB</h1>
<p>Do this first on the master.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl edit configmap -n kube-system kube-proxy
</pre>
<p>&#8230;and change <strong>strictARP </strong>to true.</p>
<pre class="brush: bash; title: ; notranslate">
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: &quot;ipvs&quot;
ipvs:
  strictARP: true
</pre>
<p>Deploy MetalLB.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.11/config/manifests/metallb-native.yaml
</pre>
<p>You&#8217;ll see the pods running in metallb-system namespace.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
kubectl get pods -n metallb-system
NAME                          READY   STATUS    RESTARTS   AGE
controller-7d56b4f464-gkfrq   1/1     Running   0          55s
speaker-l5954                 1/1     Running   0          55s
speaker-xrswp                 1/1     Running   0          55s
speaker-z8vdt                 1/1     Running   0          55s
</pre>
<p>Let&#8217;s configure our load balancer to give IPs in this 192.168.1.50-192.168.1.60 range. Change to your needs accordingly in line 8.<br />
Save it as a <strong>lb-config.yaml</strong> file.</p>
<pre class="brush: bash; highlight: [8]; title: ; notranslate">
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: first-pool
  namespace: metallb-system
spec:
  addresses:
  - 192.168.1.50-192.168.1.60
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: example
  namespace: metallb-system
</pre>
<p>Deploy the config.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl apply -f lb-config.yaml
</pre>
<p>Now, we can deploy a simple app and a load balancer. Save the file as <strong>demo.yaml</strong>.<br />
This is an app that I made in Node.js that listens on port 3000 to display the IP of the pod that the load balancer hits.<br />
You can use it for any type of a load balancer, not just MetalLB. It&#8217;s a super simple app that prints the IP of the pod where it&#8217;s running.</p>
<pre class="brush: bash; title: ; notranslate">
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
spec:
  replicas: 6
  selector:
    matchLabels:
      run: demo
  template:
    metadata:
      labels:
        run: demo
    spec:
      containers:
      - name: demo
        image: klimenta/serverip
        ports:
        - containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
  name: loadbalancer
spec:
  ports:
    - port: 80
      targetPort: 3000
      protocol: TCP
  type: LoadBalancer
  selector:
    run: demo
</pre>
<p>Deploy the app.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl apply -f demo.yaml
</pre>
<p>Check the load balancer service.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
kubectl get svc
NAME           TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)        AGE
kubernetes     ClusterIP      10.96.0.1      &lt;none&gt;         443/TCP        33m
loadbalancer   LoadBalancer   10.106.36.58   192.168.1.50   80:30770/TCP   4m33s
</pre>
<p>&#8230;and if you go to that IP (192.168.1.50) in your browser, you&#8217;ll see the IP of the pods that the load balancer hits.</p>
<h1>Install nginx ingress reverse proxy</h1>
<p>The problem with the scenario above is that for every service you need to expose, you need a different IP from the IP pool that we&#8217;ve assigned to MetalLB (.50-.60).<br />
A better solution is to install an nginx ingress that will act as a reverse proxy, but for this you&#8217;ll need a working DNS server as the services will share the same IP, but different FQDN.<br />
So, in practice it looks like this.<br />
Install nginx ingress first.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.2/deploy/static/provider/cloud/deploy.yaml
kubectl get pods -n ingress-nginx
kubectl get service ingress-nginx-controller -n=ingress-nginx
</pre>
<p>Don&#8217;t worry if the two nginx ports are not running and it says &#8211; &#8220;completed&#8221;.<br />
You&#8217;ll see the external IP that&#8217;s assigned to nginx from MetalLB.<br />
For some reason, nginx ingress won&#8217;t work without this line. Many people have this problem in a bare-metal environment and this is the workaround.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl delete -A ValidatingWebhookConfiguration ingress-nginx-admission
</pre>
<p>Now, let&#8217;s create an nginx web server. This has nothing to do with nginx ingress, it&#8217;s completely separate.<br />
I am exposing the nginx deploymenty and creating a rule that will hit the deployment on port 80 with nginx.homelab.local as FQDN.<br />
NOTE: If your browser can&#8217;t resolve this URL, it won&#8217;t work. So, make sure you have a DNS or /etc/hosts file or some type of name resolution.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl create deployment nginx --image=nginx --port=80
kubectl expose deployment nginx
kubectl create ingress nginx --class=nginx --rule nginx.homelab.local/=nginx:80
</pre>
<p>Then, let&#8217;s install Apache server that runs on the same IP and same port 80.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl create deployment httpd --image=httpd --port=80
kubectl expose deployment httpd
kubectl create ingress httpd --class=nginx --rule httpd.homelab.local/=httpd:80
</pre>
<p>If everything is OK, you&#8217;ll be able to access both web servers using their URLs.<br />
If you want to put Grafana and/or KubeCost behind nginx ingress, read these short posts <a href="https://blog.andreev.it/2023/04/general-tips-tricks-and-one-liners-part-iv/#Install_Kube_Prometheus_stack_on_NFS" rel="noopener" target="_blank">here </a>and <a href="https://blog.andreev.it/2023/04/general-tips-tricks-and-one-liners-part-iv/#Install_KubeCost_on_NFS" rel="noopener" target="_blank">here</a>.<br />
If you want to use yaml for deployment, here is an example of that. It creates 6 replicas of a pod that prints the IP where the pod is running. The service runs on port 3000. Then it creates a ClusterIP on the same port and puts the ClusterIP behind the ingress. Change the hostname at line 39.</p>
<pre class="brush: xml; title: ; notranslate">
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
spec:
  replicas: 6
  selector:
    matchLabels:
      run: demo
  template:
    metadata:
      labels:
        run: demo
    spec:
      containers:
      - name: demo
        image: klimenta/getmyip
        ports:
        - containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
  name: demo
spec:
  type: ClusterIP
  ports:
  - port: 3000
    targetPort: 3000
  selector:
    run: demo
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-demo
spec:
  rules:
  - host: demo.homelab.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: demo
            port:
              number: 3000
  ingressClassName: nginx
</pre>
<h1>SSL certificates</h1>
<p>The examples above allow you to run multiple websites behind a reverse proxy on port 80, but in reality you want everything on port 443 and with SSL certificates. I&#8217;ll describe two scenarios: with a valid cert and self-signed certificate.Fortunately, nginx ingress listens on port 443 and does automatic redirect from http->https so you don&#8217;t have to worry about any changes.</p>
<h2>Valid SSL certificate</h2>
<p>You will need a valid SSL certificate for this. And you will need the private key, the certificate, the CA root certificate and the intermediate certificate. You can get these from the vendor where you purchased the certificate. In my case, I&#8217;ll have the private key saved as domain.key, the certificate as domain.pem, the root CA certificate as domain.ca and the intermediate certificate as domain.inter.<br />
<strong>IMPORTANT</strong>: nginx expects the certificate chain in exact order: PEM -> INTERMEDIATE -> ROOT CA. If you have just the private key and the certificate (without inter and root CA), it might work, but you might get some errors when using curl, such as that the certificate authority cannot be recognized.<br />
So, create a file called domain.full.pem or whatever that has all 3 certificates in that order, e.g. <em>cat domain.pem domain.inter domain.ca > domain.full.pem.</em><br />
If you received a bundle file from the certificate vendor, create a chain from two files: domain.pem plus the bundle file. The bundle file contains the intermediate and the root certificate.<br />
Now that you have these files, you need to create a TLS secret in Kubernetes where you&#8217;ll store your certificate. You can do that with kubectl or with a YAML file.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl create secret tls demo-tls --key domain.key --cert domain.full.pem
</pre>
<p>Keep the secret, ingress and the deployment in the same namespace.<br />
If you use YAML, which most likely you&#8217;ll do if you use automation, you&#8217;ll have to encode the certificates.<br />
This is the template YAML file. Name it as <em>tls.yaml</em>.</p>
<pre class="brush: bash; title: ; notranslate">
apiVersion: v1
kind: Secret
metadata:
  name: demo-tls
type: kubernetes.io/tls
data:
  tls.crt: |
    &lt;insert_base64_string&gt;
  tls.key: |
    &lt;insert_base64_string&gt;
</pre>
<p>Don&#8217;t change data keys tls.crt and tls.key. You have to use them, not something like key.crt or something.crt. Use exactly tls.crt and tls.key.<br />
Encode the certificate with base64.</p>
<pre class="brush: bash; title: ; notranslate">
base64 -w0 domain.full.pem
</pre>
<p>Encode the key with base64.</p>
<pre class="brush: bash; title: ; notranslate">
base64 -w0 domain.key
</pre>
<p>You&#8217;ll get two long strings that start with LS0&#8230;Copy &#038; paste these strings into YAML and make sure they are properly indented.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2023/10/P166-01.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2023/10/P166-01.png" alt="" width="811" height="520" class="aligncenter size-full wp-image-9697" srcset="https://blog.andreev.it/wp-content/uploads/2023/10/P166-01.png 811w, https://blog.andreev.it/wp-content/uploads/2023/10/P166-01-300x192.png 300w, https://blog.andreev.it/wp-content/uploads/2023/10/P166-01-768x492.png 768w, https://blog.andreev.it/wp-content/uploads/2023/10/P166-01-585x375.png 585w" sizes="(max-width: 811px) 100vw, 811px" /></a><br />
Now, you can create the secret.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl apply -f tls.yaml
</pre>
<p>All yopu have to do now is to reference the TLS secret that we created. Compare the highlighted lines in this YAML with the previous one.</p>
<pre class="brush: bash; highlight: [6,7,8,9,10,11]; title: ; notranslate">
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-demo
spec:
  tls:
  - hosts:
    - demo.iandreev.com
    secretName: demo-tls
  rules:
  - host: demo.iandreev.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: demo
            port:
              number: 3000
  ingressClassName: nginx
</pre>
<p>Change the hostname in line 11 and 14 to match your cert and you can deploy the app. </p>
<h2>Self-signed certificates</h2>
<p>First, we have to create a Certificate Authority (CA). I&#8217;ll use <em>homelab.local</em> as my domain.</p>
<pre class="brush: bash; title: ; notranslate">
openssl req -x509 -nodes -sha512 -days 365 -newkey rsa:4096 \
 -keyout rootCA.key -out rootCA.crt -subj &quot;/C=US/CN=HomeLab Certificate Authority&quot;
</pre>
<p>You&#8217;ll get two files, rootCA.key (the private key) and rootCA.crt.<br />
Create a private key for the wildcard homelab.local domain and a certificate signing request file.<br />
You&#8217;ll get two files, star.homelab.local.key (the private key for the domain) and star.homelab.local.csr (the certificate signing request).</p>
<pre class="brush: bash; title: ; notranslate">
openssl req -new -nodes -sha512 -days 365 -newkey rsa:4096 \
 -keyout star.homelab.local.key -out star.homelab.local.csr -subj &quot;/C=US/ST=NJ/L=Trenton/O=HomeLab Org/CN=*.homelab.local&quot;
</pre>
<p>Create this file where you&#8217;ll specify your domain name and eventually your IP, but the IP is not necessary.</p>
<pre class="brush: bash; title: ; notranslate">
cat &lt;&lt; EOF &gt; v3.ext
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
&#x5B;alt_names]
DNS.1 = *.homelab.local
IP.1 = 192.168.1.50
EOF
</pre>
<p>Sign the certificate signing request and create the certificate file. You&#8217;ll get one certificate file star.homelab.local.crt.</p>
<pre class="brush: bash; title: ; notranslate">
openssl x509 -req -sha512 -days 365 -in star.homelab.local.csr -CA rootCA.crt -CAkey rootCA.key \
 -CAcreateserial -out star.homelab.local.crt  -extfile v3.ext
</pre>
<p>Combine the certificate and the CA certificate in a single file.</p>
<pre class="brush: bash; title: ; notranslate">
cat star.homelab.local.crt rootCA.crt &gt; star.homelab.local.chain.crt
</pre>
<p>Create this tls.yaml template file.</p>
<pre class="brush: bash; title: ; notranslate">
apiVersion: v1
kind: Secret
metadata:
  name: demo-tls
type: kubernetes.io/tls
data:
  tls.crt: |
    &lt;insert_base64_string&gt;
  tls.key: |
    &lt;insert_base64_string&gt;
</pre>
<p>Encode both the domain certificate chain file and the domain private key file.</p>
<pre class="brush: bash; title: ; notranslate">
base64 -w0 star.homelab.local.chain.crt
base64 -w0 star.homelab.local.key
</pre>
<p>You&#8217;ll get two very long single lines. Add these two lines to the tls.yaml template file but make sure they are properly indented (see the screenshot in the previous section).<br />
Now, you can create the secret.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl apply -f tls.yaml
</pre>
<p>You can also use kubectl to create the secret, see the previous section.<br />
Save this file as <em>demo-local.yaml</em> and deploy it using <strong>kubectl apply -f demo-local.yaml</strong>.</p>
<pre class="brush: bash; collapse: true; light: false; title: ; toolbar: true; notranslate">
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
spec:
  replicas: 6
  selector:
    matchLabels:
      run: demo
  template:
    metadata:
      labels:
        run: demo
    spec:
      containers:
      - name: demo
        image: klimenta/getmyip
        ports:
        - containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
  name: demo
spec:
  type: ClusterIP
  ports:
  - port: 3000
    targetPort: 3000
  selector:
    run: demo
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-demo
spec:
  tls:
  - hosts:
    - demo.homelab.local
    secretName: demo-tls
  rules:
  - host: demo.homelab.local
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: demo
            port:
              number: 3000
  ingressClassName: nginx
</pre>
<p>You&#8217;ll be able to go to your browser and check demo.homelab.local and see the page after the SSL warning.<br />
But if you double-click the rootCA.crt file on your local machine, you can import the certificate in the Trusted Root Certificates store and you won&#8217;t get that warning anymore.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2023/10/install-metallb-on-kubernetes-cluster-running-on-vmware-vms-or-bare-metal-server/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Kubernetes: Monitoring Node.js application with Prometheus and Grafana, Helm charts in AWS EKS</title>
		<link>https://blog.andreev.it/2023/01/kubernetes-monitoring-node-js-application-with-prometheus-and-grafana-helm-charts/</link>
					<comments>https://blog.andreev.it/2023/01/kubernetes-monitoring-node-js-application-with-prometheus-and-grafana-helm-charts/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Tue, 24 Jan 2023 16:01:05 +0000</pubDate>
				<category><![CDATA[Containers]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[EKS]]></category>
		<category><![CDATA[grafana]]></category>
		<category><![CDATA[helm]]></category>
		<category><![CDATA[nodejs]]></category>
		<category><![CDATA[prom-client]]></category>
		<category><![CDATA[prometheus]]></category>
		<guid isPermaLink="false">https://blog.andreev.it/?p=9312</guid>

					<description><![CDATA[Recently, we were migrating some Node.js applications to a different AWS account and we&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>Recently, we were migrating some Node.js applications to a different AWS account and we had a need to monitor the application with Prometheus and Grafana. In this post, I&#8217;ll explain how to install both Prometheus and Grafana using Helm charts, then we&#8217;ll create a simple Express.js web app, create a Helm chart for it and deploy it on an EKS cluster in AWS. I am using an EKS cluster, but you can use any Kubernetes cluster that you can manage it with kubectl. You might have to make some changes though in how you access the Load Balancer for Grafana.<br />
<strong>Requirements</strong>:</p>
<ul>
&#8211; eksctl<br />
&#8211; kubectl<br />
&#8211; helm</ul>
<h1>Create EKS cluster</h1>
<p>This is optional, if you already have a running Kubernetes cluster, you can skip this step.</p>
<pre class="brush: bash; title: ; notranslate">
eksctl create cluster --name eksTest \
--region us-east-2 \
--instance-types t3.medium \
--nodes 2 \
--managed \
--version 1.22
</pre>
<p>This command will create a 2 node cluster, with t3.medium instances in Ohio region using Amazon Linux 2 image. It will take about 15 minutes.<br />
Verify that everything looks OK.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
kubectl get nodes
NAME                                           STATUS   ROLES    AGE     VERSION
ip-192-168-10-4.us-east-2.compute.internal     Ready    &lt;none&gt;   8m54s   v1.22.15-eks-fb459a0
ip-192-168-74-160.us-east-2.compute.internal   Ready    &lt;none&gt;   8m51s   v1.22.15-eks-fb459a0
</pre>
<h1>Install Prometheus and Grafana</h1>
<p>Add the Helm repos for Prometheus, Grafana and update any changes.</p>
<pre class="brush: bash; title: ; notranslate">
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update
</pre>
<p>We&#8217;ll create a separate namespace for Prometheus and install the chart there.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl create namespace prometheus
helm install prometheus prometheus-community/prometheus \
    --namespace prometheus \
    --set alertmanager.persistentVolume.storageClass=&quot;gp2&quot; \
    --set server.persistentVolume.storageClass=&quot;gp2&quot;
</pre>
<p>Create this file. It tells Grafana what to use as a source (url). It&#8217;s the Prometheus endpoint service that we just installed.</p>
<pre class="brush: plain; title: ; notranslate">
cat &lt;&lt; EOF &gt; grafana.yaml
datasources:
  datasources.yaml:
    apiVersion: 1
    datasources:
    - name: Prometheus
      type: prometheus
      url: http://prometheus-server.prometheus.svc.cluster.local
      access: proxy
      isDefault: true
EOF
</pre>
<p>Now, let&#8217;s create a namespace for Grafana and then install Grafana in that namespace. Change the admin password. In my case it&#8217;s Password1$.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl create namespace grafana
helm install grafana grafana/grafana \
    --namespace grafana \
    --set persistence.storageClassName=&quot;gp2&quot; \
    --set persistence.enabled=true \
    --set adminPassword=&#039;Password1$&#039; \
    --values grafana.yaml \
    --set service.type=LoadBalancer
</pre>
<p>You&#8217;ll get something like this on the screen when the install ends.</p>
<pre class="brush: bash; title: ; notranslate">
export SERVICE_IP=$(kubectl get svc --namespace grafana grafana -o jsonpath=&#039;{.status.loadBalancer.ingress&#x5B;0].ip}&#039;)
</pre>
<p>That&#8217;s your load balancer IP so you can access Grafana. If you use EKS, your IP is different.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl get services -n grafana
NAME      TYPE           CLUSTER-IP     EXTERNAL-IP                                                               PORT(S)        AGE
grafana   LoadBalancer   10.100.20.53   a074996f790b74793a74bec584c04460-2034633908.us-east-2.elb.amazonaws.com   80:31916/TCP   3m21s
</pre>
<p>The IP is the gibberish URL that ends with amazonaws.com. If you go to that URL, you can log as <strong>admin </strong>and your password.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2023/01/P161-01.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2023/01/P161-01-1024x929.png" alt="" width="1024" height="929" class="aligncenter size-large wp-image-9316" srcset="https://blog.andreev.it/wp-content/uploads/2023/01/P161-01-1024x929.png 1024w, https://blog.andreev.it/wp-content/uploads/2023/01/P161-01-300x272.png 300w, https://blog.andreev.it/wp-content/uploads/2023/01/P161-01-768x696.png 768w, https://blog.andreev.it/wp-content/uploads/2023/01/P161-01-585x531.png 585w, https://blog.andreev.it/wp-content/uploads/2023/01/P161-01.png 1149w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><br />
Go to <strong>Dashboards</strong>, click on the <strong>New </strong>button on the right and click <strong>Import</strong>.<br />
Add the following dashboard IDs (one by one then click <strong>Load</strong>) and choose Prometheus as a source. The IDs are 3119 (Kubernetes cluster), 6417 (pods) and 11159 (Node.js). For the first dashboard, you&#8217;ll have some metrics. The Node.js dashboard is empty and our goal is to get the metrics from the application.</p>
<h1>Node.js application</h1>
<p>Create an empty folder and add this file.</p>
<pre class="brush: bash; title: ; notranslate">
mkdir mynodeapp
cd mynodeapp
cat &lt;&lt; EOF &gt; package.json
{
  &quot;name&quot;: &quot;mynodeapp&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;description&quot;: &quot;Node.js on Docker&quot;,
  &quot;author&quot;: &quot;Kliment Andreev &lt;kliment@andreev.it&gt;&quot;,
  &quot;main&quot;: &quot;server.js&quot;,
  &quot;scripts&quot;: {
    &quot;start&quot;: &quot;node server.js&quot;
  },
  &quot;dependencies&quot;: {
    &quot;express&quot;: &quot;^4.16.1&quot;,
    &quot;prom-client&quot;: &quot;^14.1.1&quot;
  }
}
EOF
</pre>
<p>I use Node v18.12. Install the dependencies.</p>
<pre class="brush: bash; title: ; notranslate">
npm install
</pre>
<p>This is the simple web application. It listens on port 3000.</p>
<pre class="brush: bash; title: ; notranslate">
cat &lt;&lt; &quot;EOF&quot; &gt; server.js
&#039;use strict&#039;
const express = require(&#039;express&#039;)
const prom = require(&#039;prom-client&#039;)

const collectDefaultMetrics = prom.collectDefaultMetrics();

const app = express()
const port = process.env.PORT || 3000

app.get(&#039;/&#039;, (req, res, next) =&gt; {
  res.send(&#039;Hello World!&#039;);
});

app.get(&#039;/metrics&#039;, async (req, res) =&gt; {
  try {
	  res.set(&#039;Content-Type&#039;, prom.register.contentType);
	  res.end(await prom.register.metrics());
  } 
  catch (ex) {
	  res.status(500).end(ex);
  }
});

const server = app.listen(port, () =&gt; {
  console.log(`mynodeapp listening on port ${port}!`)
})
EOF
</pre>
<p>The app is a super simple web app that prints Hello World! The important part is the <strong>/metrics</strong> router. That&#8217;s how you get the metrics from the app. When we created the package.json file, we specify the <strong>prom-client</strong> as dependency. That&#8217;s the part that collects the metrics. Go to this <a href="https://github.com/siimon/prom-client" rel="noopener" target="_blank">link </a>for more info.<br />
Start the app with <strong>node server.js</strong>.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
node server.js
mynodeapp listening on port 3000!
</pre>
<p>Go to the localhost or the server IP where this app is running and you&#8217;ll see <em>Hello World!</em> printed out.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2023/01/P161-02.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2023/01/P161-02.png" alt="" width="634" height="177" class="aligncenter size-full wp-image-9321" srcset="https://blog.andreev.it/wp-content/uploads/2023/01/P161-02.png 634w, https://blog.andreev.it/wp-content/uploads/2023/01/P161-02-300x84.png 300w, https://blog.andreev.it/wp-content/uploads/2023/01/P161-02-585x163.png 585w" sizes="(max-width: 634px) 100vw, 634px" /></a><br />
And if you go to the /metrics URL, you&#8217;ll see this text. That&#8217;s what Prometheus expects.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2023/01/P161-03.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2023/01/P161-03.png" alt="" width="874" height="325" class="aligncenter size-full wp-image-9322" srcset="https://blog.andreev.it/wp-content/uploads/2023/01/P161-03.png 874w, https://blog.andreev.it/wp-content/uploads/2023/01/P161-03-300x112.png 300w, https://blog.andreev.it/wp-content/uploads/2023/01/P161-03-768x286.png 768w, https://blog.andreev.it/wp-content/uploads/2023/01/P161-03-585x218.png 585w" sizes="(max-width: 874px) 100vw, 874px" /></a><br />
Now that we know the app is working, let&#8217;s create a Docker image.</p>
<h1>Docker image and container</h1>
<p>Create this Dockerfile.</p>
<pre class="brush: bash; title: ; notranslate">
cat &lt;&lt; EOF &gt; Dockerfile
FROM node:18-alpine

# Create app directory
WORKDIR /app

# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY package*.json ./

RUN npm install
# If you are building your code for production
# RUN npm install --only=production

# Bundle app source
COPY server.js /app

EXPOSE 3000
CMD &#x5B; &quot;npm&quot;, &quot;start&quot;, &quot;server.js&quot; ]
EOF
</pre>
<p>Create a Docker image based on the Dockerfile above. Replace the username with your Docker Hub username.</p>
<pre class="brush: bash; title: ; notranslate">
docker build -t &lt;username&gt;/mynodeapp:latest .
</pre>
<p>Run the container from the image.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
docker run -p 3000:3000 &lt;username&gt;/mynodeapp:latest

&gt; mynodeapp@1.0.0 start
&gt; node server.js server.js

mynodeapp listening on port 3000!
</pre>
<p>Use the same test as before by going to the localhost or IP URL on port 3000.<br />
If everything is OK, publish the image on Dockerhub.</p>
<pre class="brush: bash; title: ; notranslate">
docker login
docker push &lt;username&gt;/mynodeapp
</pre>
<h1>Helm chart</h1>
<p>Now that we have the Docker image, we&#8217;ll create a Helm chart so we can deploy the web app to the Kubernetes cluster.<br />
Create the necessary structure.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
helm create mynodeapp
Creating mynodeapp
</pre>
<p>Go to the sub-directory <strong>mynodeapp </strong>and edit the <strong>values.yaml</strong> file first. Look for the image key first and replace it so it looks like this. Make sure you replace the <strong><username></strong> with your Docker Hub username.</p>
<pre class="brush: bash; title: ; notranslate">
image:
  repository: &lt;username&gt;/mynodeapp
  pullPolicy: IfNotPresent
  # Overrides the image tag whose default is the chart appVersion.
  tag: &quot;latest&quot;
</pre>
<p>Then look for <strong>podAnnotations: {}</strong> line and replace it so it looks like this.</p>
<pre class="brush: bash; title: ; notranslate">
podAnnotations:
  prometheus.io/scrape: &quot;true&quot;
  prometheus.io/path: &quot;/metrics&quot;
  prometheus.io/port: &quot;3000&quot;
</pre>
<p>Look for the <strong>service </strong>parameter and change it so it looks like this.</p>
<pre class="brush: bash; title: ; notranslate">
service:
  type: LoadBalancer
  port: 80
  targetPort: 3000
  name: mynodeapp-service
</pre>
<p>Look for the resources key and uncomment the defaults.</p>
<pre class="brush: bash; title: ; notranslate">
resources: {}
  # We usually recommend not to specify default resources and to leave this as a conscious
  # choice for the user. This also increases chances charts run on environments with little
  # resources, such as Minikube. If you do want to specify resources, uncomment the following
  # lines, adjust them as necessary, and remove the curly braces after &#039;resources:&#039;.
limits:
  cpu: 100m
  memory: 128Mi
requests:
  cpu: 100m
  memory: 128Mi
</pre>
<p>Edit the <strong>templates/deployment.yaml</strong> file and find these lines.</p>
<pre class="brush: bash; title: ; notranslate">
ports:
  - name: http
    containerPort: {{ .Values.service.port }}
</pre>
<p>Replace the containerPort to look like this.</p>
<pre class="brush: bash; title: ; notranslate">
containerPort: 3000
</pre>
<p>Go back to the root of the <strong>mynodeapp </strong>folder and check the chart.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
helm install mynodeapp --generate-name
NAME: mynodeapp-1674575241
LAST DEPLOYED: Tue Jan 24 10:47:22 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
NOTES:
1. Get the application URL by running these commands:
     NOTE: It may take a few minutes for the LoadBalancer IP to be available.
           You can watch the status of by running &#039;kubectl get --namespace default svc -w mynodeapp-1674575241&#039;
  export SERVICE_IP=$(kubectl get svc --namespace default mynodeapp-1674575241 --template &quot;{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}&quot;)
  echo http://$SERVICE_IP:80
</pre>
<p>Execute the <strong>export SERVICE_IP</strong> command above and echo the IP. That&#8217;s your URL. If you go to that URL you&#8217;ll see the <em>Hello World!</em> greetings.<br />
What we care is the pod. Get the pod running. It&#8217;s in the default namespace.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
kubectl get pods
NAME                                    READY   STATUS    RESTARTS   AGE
mynodeapp-1674575241-5ff9d7469d-pddcz   1/1     Running   0          3m
</pre>
<p>Get the annotations. Replace with your pod name.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
kubectl get pod mynodeapp-1674575241-5ff9d7469d-pddcz -o jsonpath=&#039;{.metadata.annotations}&#039;
{&quot;kubernetes.io/psp&quot;:&quot;eks.privileged&quot;,&quot;prometheus.io/path&quot;:&quot;/metrics&quot;,&quot;prometheus.io/port&quot;:&quot;3000&quot;,&quot;prometheus.io/scrape&quot;:&quot;true&quot;}
</pre>
<p>You can see the prometheus annotations.<br />
And if you go to Grafana and open up the NodeJS dash you&#8217;ll see the metrics.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2023/01/P161-04.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2023/01/P161-04-1024x740.png" alt="" width="1024" height="740" class="aligncenter size-large wp-image-9327" srcset="https://blog.andreev.it/wp-content/uploads/2023/01/P161-04-1024x740.png 1024w, https://blog.andreev.it/wp-content/uploads/2023/01/P161-04-300x217.png 300w, https://blog.andreev.it/wp-content/uploads/2023/01/P161-04-768x555.png 768w, https://blog.andreev.it/wp-content/uploads/2023/01/P161-04-1170x845.png 1170w, https://blog.andreev.it/wp-content/uploads/2023/01/P161-04-585x423.png 585w, https://blog.andreev.it/wp-content/uploads/2023/01/P161-04.png 1251w" sizes="(max-width: 1024px) 100vw, 1024px" /></a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2023/01/kubernetes-monitoring-node-js-application-with-prometheus-and-grafana-helm-charts/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>AWS: EKS monitoring and logging</title>
		<link>https://blog.andreev.it/2022/10/aws-eks-monitoring-and-logging/</link>
					<comments>https://blog.andreev.it/2022/10/aws-eks-monitoring-and-logging/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Mon, 03 Oct 2022 23:09:44 +0000</pubDate>
				<category><![CDATA[AWS]]></category>
		<category><![CDATA[Cloud]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[CloudWatch]]></category>
		<category><![CDATA[EKS]]></category>
		<category><![CDATA[fluentbit]]></category>
		<category><![CDATA[grafana]]></category>
		<category><![CDATA[logging]]></category>
		<category><![CDATA[monitoring]]></category>
		<category><![CDATA[prometheus]]></category>
		<guid isPermaLink="false">https://blog.andreev.it/?p=9214</guid>

					<description><![CDATA[In this post I&#8217;ll explain several things related to EKS monitoring and logging. &#8211;&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>In this post I&#8217;ll explain several things related to EKS monitoring and logging. </p>
<ol>
&#8211; How to create an EKS cluster<br />
&#8211; Enable the EKS control plane logs and send them to CloudWatch<br />
&#8211; Send these logs from CloudWatch to OpenSearch cluster<br />
&#8211; Install Container Insights and FluentBit and send logs to CloudWatch<br />
&#8211; Install Prometheus and Grafana to monitor and visualize EKS cluster metrics<br />
&#8211; WordPress and CloudWatch log group
</ol>
<p>In order to do that, we&#8217;ll need the following CLI tools. </p>
<ol>
&#8211; eksctl<br />
&#8211; kubectl<br />
&#8211; helm<br />
&#8211; aws cli</ol>
<p>You can easily find how to install and configure these tools for various OSes. </p>
<h1>Create the EKS cluster</h1>
<p>I&#8217;ll create a managed cluster called <strong>eksWordPress </strong>in <strong>us-east-2</strong> region with <strong>two </strong><strong>t3.medium</strong> nodes. </p>
<pre class="brush: bash; title: ; notranslate">
eksctl create cluster --name eksWordPress --region us-east-2 --instance-types t3.medium --nodes 2 --managed --version 1.22
</pre>
<p>If you get an error that the last supported version is 1.21, update eksctl tool. The cluster creation took about 20 mins for me. </p>
<h1>Send EKS logs to CloudWatch</h1>
<p>Once the cluster was created and up and running, enabling the EKS control plane logs is easy. There are 5 log types (API server, Audit, Authenticator, Controller Manager and Scheduler logs). The logged info is a lot, so it&#8217;s up to you what you want to log. There is no way to choose what to log, e.g. info, warning, errors. Everything is logged. To enable logging from the console, go to the EKS cluster, select it, click the <strong>Logging</strong> tab and then click on the <strong>Manage logging</strong> button.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2022/09/P160-01.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2022/09/P160-01-1024x278.png" alt="" width="1024" height="278" class="aligncenter size-large wp-image-9230" srcset="https://blog.andreev.it/wp-content/uploads/2022/09/P160-01-1024x278.png 1024w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-01-300x81.png 300w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-01-768x208.png 768w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-01-1536x416.png 1536w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-01-2048x555.png 2048w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-01-1920x521.png 1920w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-01-1170x317.png 1170w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-01-585x159.png 585w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><br />
If you want to do it from a CLI, type this command and specify what types of logs you want sent to CloudWatch. Specify the region and the cluster name.</p>
<pre class="brush: bash; title: ; notranslate">
aws eks update-cluster-config \
    --region &lt;REGION&gt; \
    --name &lt;CLUSTER_NAME&gt; \
    --logging &#039;{&quot;clusterLogging&quot;:&#x5B;{&quot;types&quot;:&#x5B;&quot;api&quot;,&quot;audit&quot;,&quot;authenticator&quot;,&quot;controllerManager&quot;,&quot;scheduler&quot;],&quot;enabled&quot;:true}]}&#039;
</pre>
<p>In addition, because we provisioned the cluster with the <strong>eksctl</strong> tool, we can also enable the logs using <strong>eksctl</strong>.</p>
<pre class="brush: bash; title: ; notranslate">
eksctl utils update-cluster-logging --enable-types all --cluster &lt;CLUSTER_NAME&gt; --approve
</pre>
<p>Or for certain types, use&#8230; </p>
<pre class="brush: bash; title: ; notranslate">
eksctl utils update-cluster-logging --enable-types &lt;LOG_TYPE&gt; --cluster &lt;CLUSTER_NAME&gt; --approve
</pre>
<p>You can use the same types as in the AWS CLI command above (api, audit, scheduler&#8230;).<br />
If you go to CloudWatch and then the <strong>Log groups</strong>, you&#8217;ll see the log group with the name of the EKS cluster (<em>/aws/eks/eksWordPress/cluster</em>).<br />
Make sure you change the retention from <strong>Never</strong> to some value. You probably don&#8217;t want to keep these logs indefinitely. If you look at the logs streams, you&#8217;ll see that there is a lot of info there. Most of the stuff is useless. </p>
<h1>Send the logs to OpenSearch cluster</h1>
<p>Let&#8217;s create a public OpenSearch cluster with anonymous access that only you can access. Replace the IP at the end of the statement with your IP address. It takes less than 10 mins for the OpenSearch cluster to be provisioned. </p>
<pre class="brush: bash; title: ; notranslate">
aws opensearch create-domain --domain-name oswordpress --engine-version OpenSearch_1.3 \
    --auto-tune-options DesiredState=&quot;ENABLED&quot; --cluster-config InstanceType=t3.small.search,InstanceCount=2 \
    --ebs-options EBSEnabled=true,VolumeType=gp3,VolumeSize=10,Iops=3000 \
    --access-policies &#039;{&quot;Version&quot;: &quot;2012-10-17&quot;, &quot;Statement&quot;: &#x5B;{&quot;Action&quot;: &quot;es:*&quot;, &quot;Principal&quot;:&quot;*&quot;,&quot;Effect&quot;: &quot;Allow&quot;, &quot;Condition&quot;: {&quot;IpAddress&quot;:{&quot;aws:SourceIp&quot;:&#x5B;&quot;2.18.2.19/32&quot;]}}}]}&#039;
</pre>
<p>Once the cluster is provisioned you can get the public URL with:</p>
<pre class="brush: bash; title: ; notranslate">
echo `aws es describe-elasticsearch-domain --domain-name oswordpress --output text --query &quot;DomainStatus.Endpoint&quot;`\\_dashboards
</pre>
<p>You can also get the dashboard URL from the cluster settings as well.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2022/09/P160-02.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2022/09/P160-02-1024x269.png" alt="" width="1024" height="269" class="aligncenter size-large wp-image-9240" srcset="https://blog.andreev.it/wp-content/uploads/2022/09/P160-02-1024x269.png 1024w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-02-300x79.png 300w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-02-768x201.png 768w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-02-1536x403.png 1536w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-02-2048x537.png 2048w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-02-1920x504.png 1920w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-02-1170x307.png 1170w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-02-585x153.png 585w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><br />
Create the following IAM policy. Save the policy below as <strong>policy-file.json</strong> but change the <strong>account_no</strong> in line 9 to match your AWS account.</p>
<pre class="brush: xml; highlight: [9]; title: ; notranslate">
{
    &quot;Version&quot;: &quot;2012-10-17&quot;,
    &quot;Statement&quot;: &#x5B;
        {
            &quot;Action&quot;: &#x5B;
                &quot;es:*&quot;
            ],
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Resource&quot;: &quot;arn:aws:es:us-east-2:&lt;account_no&gt;:domain/oswordpress/*&quot;
        }
    ]
}
</pre>
<p>And then create the IAM policy.</p>
<pre class="brush: bash; title: ; notranslate">
aws iam create-policy --policy-name polOpenSearch --policy-document file://policy-file.json
</pre>
<p>Create the role. Save the policy below as <strong>policy-trust.json</strong>.</p>
<pre class="brush: xml; title: ; notranslate">
{
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: &#x5B;
    {
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Principal&quot;: {
        &quot;Service&quot;: &quot;lambda.amazonaws.com&quot;
      },
      &quot;Action&quot;: &quot;sts:AssumeRole&quot;
    }
  ]
}
</pre>
<p>And then create the role.</p>
<pre class="brush: bash; title: ; notranslate">
aws iam create-role --role-name rolOpenSearch --assume-role-policy file://policy-trust.json
</pre>
<p>Finally, attach the policy to the role.</p>
<pre class="brush: bash; title: ; notranslate">
aws iam put-role-policy --role-name rolOpenSearch --policy-name polOpenSearch --policy-document file://policy-file.json 
</pre>
<p>Go to <strong>CloudWatch</strong>, select the <strong>Log group</strong>, from the <strong>Actions</strong> button select <strong>Subscription filters</strong> and then <strong>Create Amazon OpenSearch service subscription filter</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2022/09/P160-03.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2022/09/P160-03-1024x355.png" alt="" width="1024" height="355" class="aligncenter size-large wp-image-9242" srcset="https://blog.andreev.it/wp-content/uploads/2022/09/P160-03-1024x355.png 1024w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-03-300x104.png 300w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-03-768x266.png 768w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-03-1536x533.png 1536w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-03-2048x711.png 2048w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-03-1920x666.png 1920w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-03-1170x406.png 1170w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-03-585x203.png 585w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><br />
Choose the OpenSearch cluster that we created, the log format is <strong>JSON</strong>, subscription filter pattern is <strong>&#8221; &#8220;</strong> for all events and type <strong>all</strong> subscription filter name or whatever you want to name this pattern. Click <strong>Start streaming</strong> button.<br />
Now, go to OpenSearch dashboard and from the hamburger menu in the upper left corner, click on <strong>Visualize</strong>. In the middle of the screen, you&#8217;ll be prompted to create an index pattern. Type <strong>cwl*</strong> and click <strong>Next step</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2022/09/P160-04.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2022/09/P160-04-1024x438.png" alt="" width="1024" height="438" class="aligncenter size-large wp-image-9249" srcset="https://blog.andreev.it/wp-content/uploads/2022/09/P160-04-1024x438.png 1024w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-04-300x128.png 300w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-04-768x329.png 768w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-04-1536x657.png 1536w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-04-2048x877.png 2048w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-04-1920x822.png 1920w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-04-1170x501.png 1170w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-04-585x250.png 585w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><br />
Select <strong>@timestamp</strong> from the drop down menu and click on <strong>Create index pattern</strong>. You&#8217;ll see the fields and the index.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2022/09/P160-05.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2022/09/P160-05-1024x659.png" alt="" width="1024" height="659" class="aligncenter size-large wp-image-9251" srcset="https://blog.andreev.it/wp-content/uploads/2022/09/P160-05-1024x659.png 1024w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-05-300x193.png 300w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-05-768x494.png 768w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-05-1536x989.png 1536w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-05-2048x1319.png 2048w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-05-1920x1236.png 1920w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-05-1170x753.png 1170w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-05-585x377.png 585w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><br />
Click on the <strong>Discover</strong> from the hamburger menu and you&#8217;ll see your data there. You can use queries to search your data, but that&#8217;s out of the scope of this post.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2022/09/P160-06.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2022/09/P160-06-1024x400.png" alt="" width="1024" height="400" class="aligncenter size-large wp-image-9252" srcset="https://blog.andreev.it/wp-content/uploads/2022/09/P160-06-1024x400.png 1024w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-06-300x117.png 300w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-06-768x300.png 768w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-06-1536x600.png 1536w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-06-2048x800.png 2048w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-06-1920x750.png 1920w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-06-1170x457.png 1170w, https://blog.andreev.it/wp-content/uploads/2022/09/P160-06-585x228.png 585w" sizes="(max-width: 1024px) 100vw, 1024px" /></a></p>
<h1>Container Insights and FluentBit</h1>
<p>Container Insights is a CloudWatch agent that we&#8217;ll install on the EKS cluster. It will collect all kinds of metrics and then FluentBit as a log forwarder will ship those logs to CloudWatch. See this link for more <a href="https://aws.amazon.com/blogs/containers/fluent-bit-integration-in-cloudwatch-container-insights-for-eks/" rel="noopener" target="_blank">info</a>.<br />
Let&#8217;s do the prerequisites work first.<br />
Get the nodes. Type the first line only, the rest is my output.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
kubectl get nodes                                                                                                                                                               
NAME                                           STATUS   ROLES    AGE     VERSION
ip-192-168-56-250.us-east-2.compute.internal   Ready    &lt;none&gt;   3h39m   v1.22.12-eks-ba74326
ip-192-168-66-251.us-east-2.compute.internal   Ready    &lt;none&gt;   3h39m   v1.22.12-eks-ba74326
</pre>
<p>Get the instance ID from any of the instances, it doesn&#8217;t matter. Replace the name accordingly after <strong>Values=</strong>.</p>
<pre class="brush: bash; highlight: [1,2]; title: ; notranslate">
aws ec2 describe-instances --filters &#039;Name=private-dns-name,Values=ip-192-168-56-250.us-east-2.compute.internal&#039; \
    --output text --query &#039;Reservations&#x5B;*].Instances&#x5B;*].InstanceId&#039;
i-05adaa0a823a8549a
</pre>
<p>Once you have the instance ID of any node, get the Arn of the IAM role that&#8217;s the attached to that node.</p>
<pre class="brush: bash; highlight: [1,5]; title: ; notranslate">
aws ec2 describe-instances --region us-east-2 --instance-ids i-05adaa0a823a8549a --query &#039;Reservations&#x5B;*].Instances&#x5B;*].IamInstanceProfile.Id&#039;
&#x5B;
    &#x5B;
        &quot;AIPAXFRN6SYD75I5N4BUN&quot;
    ]
]
</pre>
<p>Get the role name. Replace the ID (<strong>AIPAX</strong>&#8230;) with your value above.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
aws iam list-instance-profiles --query &#039;InstanceProfiles&#x5B;?InstanceProfileId==`AIPAXFRN6SYD75I5N4BUN`].Roles&#x5B;*].RoleName&#039; 
&#x5B;
    &#x5B;
        &quot;eksctl-eksWordPress-nodegroup-ng-NodeInstanceRole-LWJZESQ468Y2&quot;
    ]
]
</pre>
<p>We want to add a policy that allows nodes to write to CloudWatch logs group. Replace the <strong>&#8211;role-name </strong>with the value that you got above.</p>
<pre class="brush: bash; title: ; notranslate">
aws iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy --role-name eksctl-eksWordPress-nodegroup-ng-NodeInstanceRole-LWJZESQ468Y2
</pre>
<pre class="brush: bash; title: ; notranslate">
oidc_id=$(aws eks describe-cluster --name eksWordPress --query &quot;cluster.identity.oidc.issuer&quot; --output text | cut -d &#039;/&#039; -f 5)
</pre>
<p>Type this line and see if there is any output. If no output, execute the command after. If yes-output, then do nothing.</p>
<pre class="brush: bash; title: ; notranslate">
aws iam list-open-id-connect-providers | grep $oidc_id
</pre>
<p>If no output, type this.</p>
<pre class="brush: bash; title: ; notranslate">
eksctl utils associate-iam-oidc-provider --cluster eksWordPress --approve
</pre>
<p>Then install CloudWatch Container Insights and FluentBit. Change the <strong>CLUSTER_NAME</strong> and <strong>REGION</strong> in lines 1 and 2.</p>
<pre class="brush: bash; highlight: [1,2]; title: ; notranslate">
ClusterName=&lt;CLUSTER_NAME&gt;
RegionName=&lt;REGION&gt;
FluentBitHttpPort=&#039;2020&#039;
FluentBitReadFromHead=&#039;Off&#039;
&#x5B;&#x5B; ${FluentBitReadFromHead} = &#039;On&#039; ]] &amp;&amp; FluentBitReadFromTail=&#039;Off&#039;|| FluentBitReadFromTail=&#039;On&#039;
&#x5B;&#x5B; -z ${FluentBitHttpPort} ]] &amp;&amp; FluentBitHttpServer=&#039;Off&#039; || FluentBitHttpServer=&#039;On&#039;
curl https://raw.githubusercontent.com/aws-samples/amazon-cloudwatch-container-insights/latest/k8s-deployment-manifest-templates/deployment-mode/daemonset/container-insights-monitoring/quickstart/cwagent-fluent-bit-quickstart.yaml | sed &#039;s/{{cluster_name}}/&#039;${ClusterName}&#039;/;s/{{region_name}}/&#039;${RegionName}&#039;/;s/{{http_server_toggle}}/&quot;&#039;${FluentBitHttpServer}&#039;&quot;/;s/{{http_server_port}}/&quot;&#039;${FluentBitHttpPort}&#039;&quot;/;s/{{read_from_head}}/&quot;&#039;${FluentBitReadFromHead}&#039;&quot;/;s/{{read_from_tail}}/&quot;&#039;${FluentBitReadFromTail}&#039;&quot;/&#039; | kubectl apply -f - 
</pre>
<p>Download a config map and edit it the file called <em>cwagent-configmap.yaml</em>.</p>
<pre class="brush: bash; title: ; notranslate">
curl -O https://raw.githubusercontent.com/aws-samples/amazon-cloudwatch-container-insights/latest/k8s-deployment-manifest-templates/deployment-mode/daemonset/container-insights-monitoring/cwagent/cwagent-configmap.yaml
</pre>
<p>In line 11, change the variable so it points to your cluster. In my case it looks like this.</p>
<pre class="brush: bash; title: ; notranslate">
&quot;cluster_name&quot;: &quot;{{eksWordPress}}&quot;,
</pre>
<p>Save the changes and apply the config.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl apply -f cwagent-configmap.yaml
</pre>
<p>Then deploy it as a DaemonSet.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl apply -f https://raw.githubusercontent.com/aws-samples/amazon-cloudwatch-container-insights/latest/k8s-deployment-manifest-templates/deployment-mode/daemonset/container-insights-monitoring/cwagent/cwagent-daemonset.yaml
</pre>
<p>Verify that it&#8217;s running. Type the first line only. The rest is my output.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
kubectl get pods -n amazon-cloudwatch
NAME                     READY   STATUS    RESTARTS   AGE
cloudwatch-agent-dfkzv   1/1     Running   0          18m
cloudwatch-agent-nfnf7   1/1     Running   0          18m
fluent-bit-8vd2n         1/1     Running   0          18m
fluent-bit-tvtpv         1/1     Running   0          18m
</pre>
<p>Check the logs.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl logs &lt;POD_NAME&gt;  -n amazon-cloudwatch
</pre>
<p>Or in my case&#8230;</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
kubectl logs cloudwatch-agent-dfkzv -n amazon-cloudwatch
&#x5B;2022/10/03 13:28:56] &#x5B; info] &#x5B;output:cloudwatch_logs:cloudwatch_logs.2] Created log stream ip-192-168-73-121.us-east-2.compute.internal.host.messages
&#x5B;2022/10/03 13:29:06] &#x5B; info] &#x5B;output:cloudwatch_logs:cloudwatch_logs.0] Creating log stream ip-192-168-73-121.us-east-2.compute.internal-application.var.log.containers.cloudwatch-agent-7fvxd_amazon-cloudwatch_cloudwatch-agent-5409bed9d4733a51602e4bc0cccde5e5580eb3e9282cd5cc4c1a4f2d2e28e8ea.log in log group /aws/containerinsights/eksWordPress/application
&#x5B;2022/10/03 13:29:06] &#x5B; info] &#x5B;output:cloudwatch_logs:cloudwatch_logs.0] Created log stream ip-192-168-73-121.us-east-2.compute.internal-application.var.log.containers.cloudwatch-agent-7fvxd_amazon-cloudwatch_cloudwatch-agent-5409bed9d4733a51602e4bc0cccde5e5580eb3e9282cd5cc4c1a4f2d2e28e8ea.log
</pre>
<p>If you go to CloudWatch now you&#8217;ll see 4 new log groups.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2022/10/P160-07.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2022/10/P160-07-1024x287.png" alt="" width="1024" height="287" class="aligncenter size-large wp-image-9277" srcset="https://blog.andreev.it/wp-content/uploads/2022/10/P160-07-1024x287.png 1024w, https://blog.andreev.it/wp-content/uploads/2022/10/P160-07-300x84.png 300w, https://blog.andreev.it/wp-content/uploads/2022/10/P160-07-768x215.png 768w, https://blog.andreev.it/wp-content/uploads/2022/10/P160-07-1536x430.png 1536w, https://blog.andreev.it/wp-content/uploads/2022/10/P160-07-2048x573.png 2048w, https://blog.andreev.it/wp-content/uploads/2022/10/P160-07-1920x537.png 1920w, https://blog.andreev.it/wp-content/uploads/2022/10/P160-07-1170x327.png 1170w, https://blog.andreev.it/wp-content/uploads/2022/10/P160-07-585x164.png 585w" sizes="(max-width: 1024px) 100vw, 1024px" /></a></p>
<h1>Prometheus and Grafana</h1>
<p>The Kubernetes API can also be monitored using Prometheus. We&#8217;ll install it using helm.<br />
Create a namespace first.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl create namespace prometheus
</pre>
<p>Add the Prometheus repo. </p>
<pre class="brush: bash; title: ; notranslate">
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
</pre>
<p>Deploy Prometheus.</p>
<pre class="brush: bash; title: ; notranslate">
helm upgrade -i prometheus prometheus-community/prometheus --namespace prometheus --set alertmanager.persistentVolume.storageClass=&quot;gp2&quot;,server.persistentVolume.storageClass=&quot;gp2&quot;
</pre>
<p>Check if everything is OK.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
kubectl get pods -n prometheus
NAME                                            READY   STATUS    RESTARTS   AGE
prometheus-alertmanager-5c57cc6945-v9lcb        2/2     Running   0          4m55s
prometheus-kube-state-metrics-77ddf69b4-jsgrp   1/1     Running   0          4m55s
prometheus-node-exporter-68dk9                  1/1     Running   0          4m55s
prometheus-node-exporter-m98xk                  1/1     Running   0          4m55s
prometheus-pushgateway-ff89cc976-4sfhl          1/1     Running   0          4m55s
prometheus-server-6c99667b9b-mpw97              2/2     Running   0          4m55s
</pre>
<p>Type this command and open up a browser and go to localhost:9090.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl --namespace=prometheus port-forward deploy/prometheus-server 9090
</pre>
<p>You can CTRL-C out of the command prompt once you verify it&#8217;s OK.<br />
Let&#8217;s add the Grafana repo first.</p>
<pre class="brush: bash; title: ; notranslate">
helm repo add grafana https://grafana.github.io/helm-charts
</pre>
<p>Copy, paste and save this as <em>grafana.yaml</em>.</p>
<pre class="brush: bash; title: ; notranslate">
datasources:
  datasources.yaml:
    apiVersion: 1
    datasources:
    - name: Prometheus
      type: prometheus
      url: http://prometheus-server.prometheus.svc.cluster.local
      access: proxy
      isDefault: true
</pre>
<p>Create a namespace for grafana.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl create namespace grafana
</pre>
<p>Deploy using helm. Look at the <strong>adminPassword</strong> parameter. Change it to something else. In my case it&#8217;s <em>admin123!</em>.</p>
<pre class="brush: bash; title: ; notranslate">
helm install grafana grafana/grafana --namespace grafana --set persistence.storageClassName=&quot;gp2&quot; --set persistence.enabled=true --set adminPassword=&#039;admin123!&#039; --values grafana.yaml --set service.type=LoadBalancer
</pre>
<p>Check if everything is OK.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl get all -n grafana
</pre>
<p>Get the URL of the classic LB that was just created.</p>
<pre class="brush: bash; title: ; notranslate">
export ELB=$(kubectl get svc -n grafana grafana -o jsonpath=&#039;{.status.loadBalancer.ingress&#x5B;0].hostname}&#039;)
echo &quot;http://$ELB&quot;
</pre>
<p>Go to that URL and you&#8217;ll see the Grafana URL.<br />
You should see the Grafana landing page. Log as admin and the password you specified when pods were deployed.<br />
In Grafana create a new dashboard and <strong>Import</strong> the dashboard with ID <strong>3119</strong>. Choose <strong>Prometheus</strong> as source.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2022/10/P160-08.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2022/10/P160-08-1024x822.png" alt="" width="1024" height="822" class="aligncenter size-large wp-image-9279" srcset="https://blog.andreev.it/wp-content/uploads/2022/10/P160-08-1024x822.png 1024w, https://blog.andreev.it/wp-content/uploads/2022/10/P160-08-300x241.png 300w, https://blog.andreev.it/wp-content/uploads/2022/10/P160-08-768x616.png 768w, https://blog.andreev.it/wp-content/uploads/2022/10/P160-08-1536x1232.png 1536w, https://blog.andreev.it/wp-content/uploads/2022/10/P160-08-1170x939.png 1170w, https://blog.andreev.it/wp-content/uploads/2022/10/P160-08-585x469.png 585w, https://blog.andreev.it/wp-content/uploads/2022/10/P160-08.png 1892w" sizes="(max-width: 1024px) 100vw, 1024px" /></a><br />
For pods monitoring use the same method but this time specify <strong>6417</strong> as a dashboard ID. This is how it looks like under my account.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2022/10/P160-09.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2022/10/P160-09-976x1024.png" alt="" width="976" height="1024" class="aligncenter size-large wp-image-9281" srcset="https://blog.andreev.it/wp-content/uploads/2022/10/P160-09-976x1024.png 976w, https://blog.andreev.it/wp-content/uploads/2022/10/P160-09-286x300.png 286w, https://blog.andreev.it/wp-content/uploads/2022/10/P160-09-768x806.png 768w, https://blog.andreev.it/wp-content/uploads/2022/10/P160-09-1464x1536.png 1464w, https://blog.andreev.it/wp-content/uploads/2022/10/P160-09-1170x1227.png 1170w, https://blog.andreev.it/wp-content/uploads/2022/10/P160-09-585x614.png 585w, https://blog.andreev.it/wp-content/uploads/2022/10/P160-09.png 1468w" sizes="(max-width: 976px) 100vw, 976px" /></a></p>
<h1>WordPress</h1>
<p>Let&#8217;s deploy WordPress.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl create namespace wordpress
</pre>
<p>Add the Bitnami helm chart.</p>
<pre class="brush: bash; title: ; notranslate">
helm repo add bitnami https://charts.bitnami.com/bitnami
</pre>
<p>Deploy WordPress in its own namespace</p>
<pre class="brush: plain; title: ; notranslate">
helm -n wordpress install understood-zebu bitnami/wordpress
</pre>
<p>Wait for 3-4 mins and do this command. This is your ELB, get the URL, something like *zdasdfa*.elb.amazonaws.com</p>
<pre class="brush: bash; title: ; notranslate">
kubectl get svc --namespace wordpress -w understood-zebu-wordpress
</pre>
<p>The username is <strong>user</strong> and get the password with:</p>
<pre class="brush: bash; title: ; notranslate">
echo Password: $(kubectl get secret --namespace wordpress understood-zebu-wordpress -o jsonpath=&quot;{.data.wordpress-password}&quot; | base64 -d)
</pre>
<p>If you go to the ELB URL, you&#8217;ll hit WordPress main page, if you want to login, add <strong>/wp-login.php</strong> as suffix to the above URL.<br />
Go to CloudWatch and check the <strong>/aws/containerinsights/eksWordPress/application</strong> log group. You&#8217;ll see a bunch of references for WordPress. You can ship those to OpenSearch if you want and alert on errors or whatever you want to do.</p>
<h1>Delete EKS and OpenSearch cluster</h1>
<p>Detach the policy, delete the daemonset and delete the EKS cluster.</p>
<pre class="brush: bash; title: ; notranslate">
aws iam detach-role-policy --policy-arn arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy --role-name eksctl-eksWordPress-nodegroup-ng-NodeInstanceRole-LWJZESQ468Y2
ClusterName=&lt;CLUSTER_NAME&gt;
RegionName=&lt;REGION&gt;
FluentBitHttpPort=&#039;2020&#039;
FluentBitReadFromHead=&#039;Off&#039;
&#x5B;&#x5B; ${FluentBitReadFromHead} = &#039;On&#039; ]] &amp;&amp; FluentBitReadFromTail=&#039;Off&#039;|| FluentBitReadFromTail=&#039;On&#039;
&#x5B;&#x5B; -z ${FluentBitHttpPort} ]] &amp;&amp; FluentBitHttpServer=&#039;Off&#039; || FluentBitHttpServer=&#039;On&#039;
curl https://raw.githubusercontent.com/aws-samples/amazon-cloudwatch-container-insights/latest/k8s-deployment-manifest-templates/deployment-mode/daemonset/container-insights-monitoring/quickstart/cwagent-fluent-bit-quickstart.yaml | sed &#039;s/{{cluster_name}}/&#039;${ClusterName}&#039;/;s/{{region_name}}/&#039;${LogRegion}&#039;/;s/{{http_server_toggle}}/&quot;&#039;${FluentBitHttpServer}&#039;&quot;/;s/{{http_server_port}}/&quot;&#039;${FluentBitHttpPort}&#039;&quot;/;s/{{read_from_head}}/&quot;&#039;${FluentBitReadFromHead}&#039;&quot;/;s/{{read_from_tail}}/&quot;&#039;${FluentBitReadFromTail}&#039;&quot;/&#039; | kubectl delete -f -
eksctl delete cluster --name eksWordPress --region=us-east-2
</pre>
<p>Delete policy.</p>
<pre class="brush: bash; title: ; notranslate">
aws iam delete-policy --policy-arn arn:aws:iam::492943873543:policy/polOpenSearch
</pre>
<p>Delete CloudWatch log groups.</p>
<pre class="brush: bash; title: ; notranslate">
EKS_CLUSTER=eksWordPress
aws logs delete-log-group --log-group-name &quot;/aws/containerinsights/$EKS_CLUSTER/application&quot;
aws logs delete-log-group --log-group-name &quot;/aws/containerinsights/$EKS_CLUSTER/dataplane&quot;
aws logs delete-log-group --log-group-name &quot;/aws/containerinsights/$EKS_CLUSTER/host&quot;
aws logs delete-log-group --log-group-name &quot;/aws/containerinsights/$EKS_CLUSTER/performance&quot;
aws logs delete-log-group --log-group-name &quot;/aws/eks/$EKS_CLUSTER/cluster&quot;
</pre>
<p>Delete the OpenSearch cluster.</p>
<pre class="brush: bash; title: ; notranslate">
aws opensearch delete-domain --domain-name oswordpress
</pre>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2022/10/aws-eks-monitoring-and-logging/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>CentOS: Install Docker and Kubernetes</title>
		<link>https://blog.andreev.it/2020/09/centos-install-docker-and-kubernetes/</link>
					<comments>https://blog.andreev.it/2020/09/centos-install-docker-and-kubernetes/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Sat, 26 Sep 2020 18:58:12 +0000</pubDate>
				<category><![CDATA[Docker]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[CentOS 8]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[k8s]]></category>
		<guid isPermaLink="false">https://blog.andreev.it/?p=7122</guid>

					<description><![CDATA[I have another post regarding Docker and Kubernetes, but it&#8217;s related to CentOS 7.&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>I have <a href="https://blog.andreev.it/?p=5127" rel="noopener noreferrer" target="_blank">another post</a> regarding Docker and Kubernetes, but it&#8217;s related to CentOS 7. In this post, I&#8217;ll explain how to install Docker and Kubernetes on CentOS 8. There is not that much difference in the installation process, but some things changed so it&#8217;s easier to write a new post than modify the original one.<br />
I&#8217;ll explain how to install Docker and Kubernetes on one master and one node. You can have as many nodes you want. All commands will be executed as root but we&#8217;ll create a user called <strong>devops </strong>that will ultimately manage both Docker and Kubernetes.</p>
<h1>Docker</h1>
<p>Log as root and execute this command on both master and the node server. The highlighted line is what you type. The rest is the output.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
Adding repo from: https://download.docker.com/linux/centos/docker-ce.repo
</pre>
<p>Then install docker. This will also install Docker CLI and containerd.io. Do this on both servers too.</p>
<pre class="brush: bash; title: ; notranslate">
dnf -y install docker-ce
</pre>
<p>Let&#8217;s make sure that it starts now and on boot.</p>
<pre class="brush: bash; title: ; notranslate">
systemctl enable --now docker
</pre>
<p>If you check the status, you&#8217;ll see that Docker is running.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
systemctl status docker | grep active
   Active: active (running) since Sat 2020-09-26 09:28:50 EDT; 50s ago
</pre>
<p>Let&#8217;s make sure it&#8217;s running and it&#8217;s functioning. Do this on one server.</p>
<pre class="brush: bash; title: ; notranslate">
docker run hello-world
</pre>
<p>You&#8217;ll see a bunch of output, some greetings and Hello from Docker! message. You don&#8217;t need this container and image so you can delete it.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS                          PORTS               NAMES
bb436fac0476        hello-world         &quot;/hello&quot;            About a minute ago   Exited (0) About a minute ago                       quirky_chatelet
</pre>
<p>Look at the end. In my case, the container name is quirky_chatelet. In your case it&#8217;s something different. Remove the container.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
docker rm quirky_chatelet
quirky_chatelet
</pre>
<p>Remove the image.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
docker rmi hello-world
Untagged: hello-world:latest
Untagged: hello-world@sha256:4cf9c47f86df71d48364001ede3a4fcd85ae80ce02ebad74156906caff5378bc
Deleted: sha256:bf756fb1ae65adf866bd8c456593cd24beb6a0a061dedf42b26a993176745f6b
Deleted: sha256:9c27e219663c25e0f28493790cc0b88bc973ba3b1686355f221c38a36978ac63
</pre>
<p>We also have to make some changes how Docker uses the storage and cgroups. By default, it&#8217;s not using systemd.</p>
<pre class="brush: bash; highlight: [1,3]; title: ; notranslate">
docker info | grep Storage
 Storage Driver: overlay2
docker info | grep Cgroup
 Cgroup Driver: cgroupfs
</pre>
<p>Overlay2 is fine but cgroupfs is not. Make these changes on both servers. Just copy and paste the whole snippet and it will create a file for you.</p>
<pre class="brush: bash; title: ; notranslate">
cat &lt;&lt;EOF &gt; /etc/docker/daemon.json
{
  &quot;exec-opts&quot;: &#x5B;&quot;native.cgroupdriver=systemd&quot;],
  &quot;log-driver&quot;: &quot;json-file&quot;,
  &quot;log-opts&quot;: {
    &quot;max-size&quot;: &quot;100m&quot;
  },
  &quot;storage-driver&quot;: &quot;overlay2&quot;,
  &quot;storage-opts&quot;: &#x5B;
    &quot;overlay2.override_kernel_check=true&quot;
  ]
}
EOF
</pre>
<p>Restart Docker daemon on both servers and check the changes.</p>
<pre class="brush: bash; highlight: [1,2]; title: ; notranslate">
systemctl restart docker
docker info | grep -i cgroup
 Cgroup Driver: systemd
</pre>
<p>Finally, we need a user that will manage Docker. We don&#8217;t want to use root for that. If you don&#8217;t have a user created, do it with <strong>adduser </strong>command. In addition, we need to add this user to be a member of the group docker that was created when we installed docker. In my case the user is <strong>devops</strong>. Do this on both servers.</p>
<pre class="brush: bash; title: ; notranslate">
usermod -aG docker devops
</pre>
<p>If you log as the devops user and execute <strong>docker ps -a</strong>, you&#8217;ll see that Docker accepted your command. Otherwise, you&#8217;ll see some access denied error.</p>
<h1>Kubernetes</h1>
<p>In order to run Kubernetes (k8s) we need to make some changes to the OS. There are several prerequisites.<br />
First, we&#8217;ll have to enable bridge networking. Do this on both servers and make sure you are logged as root.</p>
<pre class="brush: bash; title: ; notranslate">
modprobe br_netfilter
echo '1' &gt; /proc/sys/net/bridge/bridge-nf-call-iptables
echo '1' &gt; /proc/sys/net/bridge/bridge-nf-call-ip6tables
</pre>
<p>Then we have to disable SELinux security.</p>
<pre class="brush: bash; title: ; notranslate">
setenforce 0
sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config
</pre>
<p>We also need to disable swap disk.</p>
<pre class="brush: bash; title: ; notranslate">
swapoff -a
sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab
</pre>
<p>Now it&#8217;s time to install Kubernetes. Copy &#038; paste this snippet that will add the Kubernetes repo to CentOS. Do this on both servers logged as root.</p>
<pre class="brush: bash; title: ; notranslate">
cat &lt;&lt;EOF &gt; /etc/yum.repos.d/kubernetes.repo
&#x5B;kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
EOF
</pre>
<p>Install Kubernetes on both. </p>
<pre class="brush: bash; title: ; notranslate">
dnf install -y kubelet kubeadm kubectl --disableexcludes=kubernetes
</pre>
<p>(Optional) Install this handy tool that can help with Kubernetes commands auto-complete. For example, you type kubectl then g hit tab and it will give you a choice of commands that start with g.</p>
<pre class="brush: bash; title: ; notranslate">
dnf install -y bash-completion
</pre>
<p>Before we finish the installation, we&#8217;ll have to make some changes to the firewall. If you don&#8217;t have the firewall enabled, you can skip this section.</p>
<h2>Firewall</h2>
<p>On the master node only, open these ports.</p>
<pre class="brush: bash; title: ; notranslate">
firewall-cmd --add-port=6443/tcp --permanent
firewall-cmd --add-port=2379-2380/tcp --permanent
firewall-cmd --add-port=10250-10252/tcp --permanent
firewall-cmd --reload
</pre>
<p>On the worker nodes only, open these ports.</p>
<pre class="brush: bash; title: ; notranslate">
firewall-cmd --add-port=10250/tcp --permanent
firewall-cmd --add-port=30000-32767/tcp --permanent
firewall-cmd --reload
</pre>
<h2>Network</h2>
<p>Now, it&#8217;s time to install the networking component. Kubernetes has several of them, such as flannel, callico, WeaveNet etc&#8230; We&#8217;ll go with flannel.<br />
Do this on the master node only. There is no need to install the network component on the worker nodes. Kubernetes will take care of that.<br />
Initialize flannel with this subnet on the master node only. This subnet will be where pods will be running. It takes a couple of minutes.</p>
<pre class="brush: bash; title: ; notranslate">
kubeadm init --pod-network-cidr=10.244.0.0/16
</pre>
<p>At the end you&#8217;ll see an output that tells how to join the worker nodes to the master node. In my case it was something like this. Copy this code and the token somewhere safe. You&#8217;ll need it anytime you need to join a worker node. Don&#8217;t execute this yet.</p>
<pre class="brush: bash; title: ; notranslate">
kubeadm join 192.168.1.115:6443 --token dgwe20.8pw4vd8ks2savzbf \
    --discovery-token-ca-cert-hash sha256:6948547d7ff058a56aba374f44a583459f87e63aacaa53179e7adfeb5cf935a6
</pre>
<p>Let&#8217;s start Kubernetes and make sure it starts on boot. Do this on the master and the worker node logged as root.</p>
<pre class="brush: bash; title: ; notranslate">
systemctl enable --now kubelet
</pre>
<p>On the master node, log as your standard user that you made to run Docker, in my case it was devops.<br />
Run these commands, one by one.</p>
<pre class="brush: bash; title: ; notranslate">
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
echo 'source &lt;(kubectl completion bash)' &gt;&gt;~/.bashrc
</pre>
<p>Now, we have to install flannel. Do this on the master node only logged as the regular user.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
</pre>
<p>Check the nodes.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
kubectl get nodes
NAME                    STATUS   ROLES    AGE   VERSION
jupiter.andreev.local   Ready    master   12m   v1.19.2
</pre>
<p>As you can see, we have only the master node. Go to the worker node and as root run this command. The command that you saved somewhere.</p>
<pre class="brush: bash; title: ; notranslate">
kubeadm join 192.168.1.115:6443 --token dgwe20.8pw4vd8ks2savzbf \
    --discovery-token-ca-cert-hash sha256:6948547d7ff058a56aba374f44a583459f87e63aacaa53179e7adfeb5cf935a6
</pre>
<p>After a minute or two, if you run the same command (get nodes) again, you&#8217;ll see the worker node too.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
kubectl get nodes
NAME                     STATUS   ROLES    AGE     VERSION
ganymede.andreev.local   Ready    &lt;none&gt;   47s     v1.19.2
jupiter.andreev.local    Ready    master   3m47s   v1.19.2
</pre>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2020/09/centos-install-docker-and-kubernetes/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>DevOps: CI/CD using git, Jenkins, Docker, Kubernetes</title>
		<link>https://blog.andreev.it/2019/11/devops-ci-cd-using-git-jenkins-docker-kubernetes/</link>
					<comments>https://blog.andreev.it/2019/11/devops-ci-cd-using-git-jenkins-docker-kubernetes/#comments</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Sun, 03 Nov 2019 21:34:00 +0000</pubDate>
				<category><![CDATA[Containers]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Node.js]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[chat]]></category>
		<category><![CDATA[containers]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[git]]></category>
		<category><![CDATA[Jenkins]]></category>
		<guid isPermaLink="false">https://blog.andreev.it/?p=5869</guid>

					<description><![CDATA[In this post I&#8217;ll explain how to create a Node.js chat program, put the&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>In this post I&#8217;ll explain how to create a Node.js chat program, put the source on Github and then using Jenkins we&#8217;ll deploy the code to Docker Hub and from there to Docker containers that are managed by Kubernetes. There are many prerequisites for this. </p>
<ul>
&#8211; Linux workstation with Node.js and git installed (I&#8217;ll use CentOS 7)<br />
&#8211; Valid <a href="https://github.com/" rel="noopener noreferrer" target="_blank">GitHub </a>account. Create a repo called chitchat.<br />
&#8211; Valid <a href="https://hub.docker.com/" rel="noopener noreferrer" target="_blank">DockerHub </a>account. Create a repo called chitchat.<br />
&#8211; Jenkins server with a public IP, git and Docker installed (I&#8217;ll use CentOS 7)<br />
&#8211; Kubernetes cluster running Docker</ul>
<p>See my other posts on how to install <a href="https://blog.andreev.it/?p=5482" rel="noopener noreferrer" target="_blank">Jenkins </a>and <a href="https://blog.andreev.it/?p=5127" rel="noopener noreferrer" target="_blank">Kubernetes </a>cluster. </p>
<h1>The chat program</h1>
<p>For this purpose I&#8217;ve found a simple Node.js chat program that uses Express framework. The program can be found <a href="https://sabe.io/tutorials/how-to-build-real-time-chat-app-node-express-socket-io" rel="noopener noreferrer" target="_blank">here</a>, but we&#8217;ll modify it a bit. On a Linux workstation (I use CentOS, so some commands might differ), log as your regular user and create some necessary directories, a directory called chitchat and two subdirectories, public and test and two subdirectories for public, called js and css.</p>
<pre class="brush: bash; title: ; notranslate">
mkdir -p chitchat/test
mkdir -p chitchat/public/{js,css} 
cd chitchat
</pre>
<p>Create the source files for the chat program. First, let&#8217;s create the HTML. Click the (+) sign to expand. Just copy and paste everything. The first line (cat) will take care of the file creation.</p>
<pre class="brush: xml; collapse: true; light: false; title: ; toolbar: true; notranslate">
cat &lt;&lt; EOF &gt; index.html
&lt;!DOCTYPE html&gt;
&lt;html&gt;
    &lt;head&gt;
        &lt;title&gt;Chit Chat App&lt;/title&gt;
        &lt;link href=&quot;css/styles.css&quot; rel=&quot;stylesheet&quot; type=&quot;text/css&quot; media=&quot;screen&quot;&gt;
        &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width,minimum-scale=1,initial-scale=1&quot;&gt;
    &lt;/head&gt;
    &lt;body&gt;
        &lt;ul class=&quot;messages&quot;&gt;&lt;/ul&gt;
        &lt;form&gt;
            &lt;input type=&quot;text&quot; class=&quot;input&quot; autocomplete=&quot;off&quot; autofocus /&gt;
            &lt;button&gt;Send&lt;/button&gt;
        &lt;/form&gt;
        &lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.dev.js&quot;&gt;&lt;/script&gt;
        &lt;script src=&quot;js/app.js&quot;&gt;&lt;/script&gt;
    &lt;/body&gt;
&lt;/html&gt;
EOF
</pre>
<p>Then, the Node.js code.</p>
<pre class="brush: jscript; collapse: true; light: false; title: ; toolbar: true; notranslate">
cat &lt;&lt; EOF &gt; index.js
var express = require('express');
var app = express();
var http = require('http').Server(app);
var io = require('socket.io')(http);
var port = process.env.PORT || 4141;
var path = require('path');

app.get('/', function(req, res){
    res.sendFile(__dirname + '/index.html');
});

app.use(express.static(path.join(__dirname + '/public')));


io.on('connection', function(socket) {

    socket.on('user_join', function(data) {
        this.username = data;
        socket.broadcast.emit('user_join', data);
    });

    socket.on('chat_message', function(data) {
        data.username = this.username;
        socket.broadcast.emit('chat_message', data);
    });

    socket.on('disconnect', function(data) {
        socket.broadcast.emit('user_leave', this.username);
    });
});

http.listen(port, function() {
    console.log('Listening on *:' + port);
});
EOF
</pre>
<p>Then, the actual code for the app.</p>
<pre class="brush: jscript; collapse: true; light: false; title: ; toolbar: true; notranslate">
cat &lt;&lt; EOF &gt; public/js/app.js
const form = document.querySelector(&quot;form&quot;);
const input = document.querySelector(&quot;.input&quot;);
const messages = document.querySelector(&quot;.messages&quot;);
const username = prompt(&quot;Please enter a nickname: &quot;, &quot;&quot;);
const socket = io();

form.addEventListener(&quot;submit&quot;, function(event) {
        event.preventDefault();

        addMessage(username + &quot;: &quot; + input.value);

        socket.emit(&quot;chat_message&quot;, {
                        message: input.value
        });

        input.value = &quot;&quot;;
        return false;
}, false);

socket.on(&quot;chat_message&quot;, function(data) {
        addMessage(data.username + &quot;: &quot; + data.message);
});

socket.on(&quot;user_join&quot;, function(data) {
        addMessage(data + &quot; just joined the chat!&quot;);
});

socket.on(&quot;user_leave&quot;, function(data) {
        addMessage(data + &quot; has left the chat.&quot;);
});

addMessage(&quot;You have joined the chat as '&quot; + username  + &quot;'.&quot;);
socket.emit(&quot;user_join&quot;, username);

function addMessage(message) {
        const li = document.createElement(&quot;li&quot;);
        li.innerHTML = message;
        messages.appendChild(li);
        window.scrollTo(0, document.body.scrollHeight);
}
EOF
</pre>
<p>And finally the CSS stylesheet.</p>
<pre class="brush: css; collapse: true; light: false; title: ; toolbar: true; notranslate">
cat &lt;&lt; EOF &gt; public/css/styles.css
body {
    margin: 0;
    font-family: sans-serif;
}

form {
    position: fixed;
    bottom: 0;
    left: 0;
    width: 100%;
    display: flex;
    box-sizing: border-box;
    padding: 0.25rem;
}

form input {
    border: 0;
    padding: 0.5rem;
    width: 100%;
    outline: 0;
    margin-right: 0.5rem;
    border-radius: 0.25rem;
    background: #ccc;
}

form button {
    width: 6rem;
    background-color: #1b8c00;
    color: white;
    border: none;
    padding: 0.5rem;
    cursor: pointer;
    border-radius: 0.25rem;
    text-transform: uppercase;
}

form button:hover {
    background-color: #166d01;
}

.messages {
    margin: 0;
    padding: 0;
    margin-bottom: 3rem;
}

.messages li {
    padding: 0.5rem;
}

.messages li:nth-child(odd) {
    background: #eee;
}
EOF
</pre>
<p>If you don&#8217;t have Node.js installed, then install it. If you already have Node.js, skip this step. While still logged as the regular user, sudo to install Node.js and npm and check the versions. Make sure you are in chitchat directory.</p>
<pre class="brush: bash; title: ; notranslate">
curl -sL https://rpm.nodesource.com/setup_10.x | sudo bash -
sudo yum -y install nodejs
node -v &amp;&amp; npm -v
</pre>
<p>Initiate a new project. Make sure you are in chitchat directory.</p>
<pre class="brush: bash; title: ; notranslate">
npm init
</pre>
<p>Answer the questions for the initialization. Just hit Enter for every question, we&#8217;ll replace this file later. Then install Express and socket.io.</p>
<pre class="brush: bash; title: ; notranslate">
npm install -save express
npm install -save socket.io
</pre>
<p>Finally, start the program. If you have a firewall enabled, you have to poke a hole.</p>
<pre class="brush: bash; title: ; notranslate">
sudo firewall-cmd --add-port=4141/tcp --zone=public --permanent
sudo firewall-cmd --reload
node index.js
</pre>
<p>The program should listen on port 4141. Open up a browser and access the Node.js app. (<strong>http://workstation_ip:4141</strong>).<br />
I&#8217;ve opened two windows and logged as UserA and UserB to test the functionality. It looks like this.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-01.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-01.png" alt="" width="562" height="386" class="aligncenter size-full wp-image-5924" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-01.png 562w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-01-300x206.png 300w" sizes="(max-width: 562px) 100vw, 562px" /></a></p>
<h1>The GitHub repository</h1>
<p>Now that we have the program ready, let&#8217;s upload it on GitHub. Log with your account and create a new repository. In my case, I&#8217;ll name it chitchat.<br />
On your workstation, initiate the git repo, add the files, do the first commit and push the files to GitHub. It should look like this. Replace your username and the repo name in line 5.</p>
<pre class="brush: bash; highlight: [5]; title: ; notranslate">
cd chitchat
git init
git add .
git commit -m &quot;First commit.&quot;
git remote add origin https://github.com/&lt;your_username&gt;/&lt;your_repo&gt;
git push -u origin master
</pre>
<h1>Jenkins</h1>
<h2>Install Node.js plugin, Git and Docker</h2>
<p>If you installed Jenkins with the default settings, the Git and Docker plugins are probably installed. Node.js plugin is not installed by default so we&#8217;ll have to take care of that. Go to <strong>Manage Jenkins</strong> from the menu and then <strong>Manage Plugins</strong>. From the <strong>Available</strong> tab, find the NodeJS plugin and install it.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-03.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-03.png" alt="" width="495" height="83" class="aligncenter size-full wp-image-5927" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-03.png 495w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-03-300x50.png 300w" sizes="(max-width: 495px) 100vw, 495px" /></a><br />
Again, go to <strong>Manage Jenkins</strong>, <strong>Global Tool Configuration</strong> and click on <strong>Add NodeJS</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/11/P138-16.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/11/P138-16.jpg" alt="" width="438" height="116" class="aligncenter size-full wp-image-5965" srcset="https://blog.andreev.it/wp-content/uploads/2019/11/P138-16.jpg 438w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-16-300x79.jpg 300w" sizes="(max-width: 438px) 100vw, 438px" /></a><br />
Enter a name and choose the the latest 10.x version. Make sure that the <strong>Install automatically</strong> is checked. Also, I named my Node.js installation &#8220;<strong>node</strong>&#8221; and I&#8217;ll reference it as &#8220;<strong>node</strong>&#8221; further in the pipeline script. If you name your installation differently, you&#8217;ll have to change that in line 4 in the script below.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/11/P138-17.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/11/P138-17.png" alt="" width="769" height="410" class="aligncenter size-full wp-image-5986" srcset="https://blog.andreev.it/wp-content/uploads/2019/11/P138-17.png 769w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-17-300x160.png 300w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-17-768x409.png 768w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-17-585x312.png 585w" sizes="(max-width: 769px) 100vw, 769px" /></a><br />
Again, go to <strong>Manage Jenkins</strong> and then <strong>Manage Plugins</strong> in the center. Click on the <strong>Installed </strong>tab and look for Docker Pipeline plugin and Git plugin. They should be there and already installed (see the grayed checkmark on the left).<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/11/P138-18.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/11/P138-18.jpg" alt="" width="429" height="138" class="aligncenter size-full wp-image-5989" srcset="https://blog.andreev.it/wp-content/uploads/2019/11/P138-18.jpg 429w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-18-300x97.jpg 300w" sizes="(max-width: 429px) 100vw, 429px" /></a><br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/11/P138-19.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/11/P138-19.jpg" alt="" width="692" height="62" class="aligncenter size-full wp-image-5990" srcset="https://blog.andreev.it/wp-content/uploads/2019/11/P138-19.jpg 692w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-19-300x27.jpg 300w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-19-585x52.jpg 585w" sizes="(max-width: 692px) 100vw, 692px" /></a><a href="https://blog.andreev.it/wp-content/uploads/2019/11/P138-20.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/11/P138-20.jpg" alt="" width="679" height="61" class="aligncenter size-full wp-image-5991" srcset="https://blog.andreev.it/wp-content/uploads/2019/11/P138-20.jpg 679w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-20-300x27.jpg 300w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-20-585x53.jpg 585w" sizes="(max-width: 679px) 100vw, 679px" /></a><br />
If not, click on the <strong>Available </strong>tab, search for these plugins and install them similarly to what you did with Node.js plugin. Once installed, SSH to the Jenkins server and install Git first. We&#8217;ll install it from the source. This is for CentOS 7 only.</p>
<pre class="brush: bash; title: ; notranslate">
yum -y groupinstall &quot;Development Tools&quot;
yum -y install gettext-devel openssl-devel perl-CPAN perl-devel zlib-devel wget
wget https://github.com/git/git/archive/v2.23.0.tar.gz -O /tmp/git.tgz
cd /tmp
tar xzvf git.tgz 
cd git*
make configure
./configure
make install
cd /tmp
rm -Rf git*
</pre>
<p>Run <strong>git &dash;&dash;version</strong> to check if it works properly.<br />
Then, install Docker.</p>
<pre class="brush: bash; title: ; notranslate">
yum install -y yum-utils device-mapper-persistent-data lvm2
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum -y install docker-ce
systemctl enable docker
systemctl start docker
usermod jenkins -g docker
systemctl restart jenkins
</pre>
<p>Now, Jenkins can execute Docker commands.</p>
<h2>Jenkins pipeline</h2>
<p>Once installed from the main menu, in the upper-left corner, click on <strong>New Item</strong>. Enter the project name and choose <strong>Pipeline</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-02.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-02.png" alt="" width="468" height="299" class="aligncenter size-full wp-image-5926" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-02.png 468w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-02-300x192.png 300w" sizes="(max-width: 468px) 100vw, 468px" /></a><br />
Scroll down until you find the <strong>Pipeline script</strong> and paste this code. We just want to test pulling the repo from Github. Make sure you replace your username and repo name in line 10.</p>
<pre class="brush: bash; highlight: [10]; title: ; notranslate">
pipeline {
    agent any

    tools {nodejs &quot;node&quot;}

    stages {

        stage('Cloning Git') {
            steps {
                git 'https://github.com/&lt;your_name&gt;/&lt;your_repo&gt;'
            }
        }
    }
}
</pre>
<p>It looks like this.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-04.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-04.png" alt="" width="806" height="561" class="aligncenter size-full wp-image-5928" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-04.png 806w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-04-300x209.png 300w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-04-768x535.png 768w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-04-585x407.png 585w" sizes="(max-width: 806px) 100vw, 806px" /></a><br />
Click on <strong>Save </strong>and once back, click on <strong>Build Now</strong> from the menu.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-05.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-05.png" alt="" width="257" height="375" class="aligncenter size-full wp-image-5929" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-05.png 257w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-05-206x300.png 206w" sizes="(max-width: 257px) 100vw, 257px" /></a><br />
Once completed, you&#8217;ll see that everything is running OK.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-06.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-06.png" alt="" width="604" height="303" class="aligncenter size-full wp-image-5930" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-06.png 604w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-06-300x150.png 300w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-06-585x293.png 585w" sizes="(max-width: 604px) 100vw, 604px" /></a><br />
Click on <strong>Console Output</strong> to see the actual logs from git.<br />
Now, that we have everything verified, we want to add another step of testing the application. We&#8217;ll have to install the pre-requisites for the Node.js and create a test scenario. On the workstation where your project is, create a test scenario. We&#8217;ll use <a href="https://mochajs.org/" rel="noopener noreferrer" target="_blank">mocha</a> and <a href="https://www.chaijs.com/" rel="noopener noreferrer" target="_blank">chai</a> for this. </p>
<pre class="brush: bash; title: ; notranslate">
cat &lt;&lt; EOF &gt; test/test.js
var chai = require('chai');
var io = require('socket.io-client')
var app = require('../index');
var client = io('http://localhost:4141');

console.log('Server started...Waiting for client connection...');
it('Client connected...', function(done) {
  client.on('connect', function (data) {
    done();
  });
});
EOF
</pre>
<p>We also need to define the pre-requisites. Change some of the lines here to suit your needs. This is the file that we are replacing, the one that <strong>npm init</strong> creates.</p>
<pre class="brush: xml; title: ; notranslate">
cat &lt;&lt; EOF &gt; package.json
{
  &quot;name&quot;: &quot;chitchat&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;description&quot;: &quot;A simple chat program&quot;,
  &quot;main&quot;: &quot;index.js&quot;,
  &quot;scripts&quot;: {
    &quot;start&quot;: &quot;node index.js&quot;,
    &quot;test&quot;: &quot;mocha -R spec test/test.js --exit&quot;
  },
  &quot;author&quot;: &quot;Kliment Andreev&quot;,
  &quot;license&quot;: &quot;ISC&quot;,
  &quot;dependencies&quot;: {
    &quot;express&quot;: &quot;^4.17.1&quot;,
    &quot;socket.io&quot;: &quot;^2.3.0&quot;
  },
  &quot;devDependencies&quot;: {
    &quot;chai&quot;: &quot;^4.2.0&quot;,
    &quot;mocha&quot;: &quot;^5.2.0&quot;
  }
}
EOF
</pre>
<p>As you can see the first file goes under chitchat/test directory and the package.json file is in the root of chitchat directory. Don&#8217;t forget to push these files to GitHub.</p>
<pre class="brush: bash; title: ; notranslate">
git add .
git commit -m &quot;Added package.json and test unit&quot;
git push -u origin master
</pre>
<p>Now, go back to your project in Jenkins (click <strong>Configure </strong>on the left) and replace the pipeline script with this one. Make sure you change your name and repo in line 10.</p>
<pre class="brush: bash; collapse: true; highlight: [10]; light: false; title: ; toolbar: true; notranslate">
pipeline {
    agent any

    tools {nodejs &quot;node&quot;}

    stages {

        stage('Cloning Git') {
            steps {
                git 'https://github.com/&lt;your_name&gt;/&lt;your_repo&gt;'
            }
        }

        stage('Install dependencies') {
            steps {
                sh 'npm install'
            }
        }

        stage('Test') {
            steps {
                sh 'npm test'
            }
        }
    }
}
</pre>
<p>Click on <strong>Build Now</strong> and you&#8217;ll see that all stages completed fine. But, we want to go a step further and automate the build process anytime we make a change in the source code. In order to do that we need a webhook between GitHub and our Jenkins server. Make sure your Jenkins server is publicly available, otherwise this integration won&#8217;t work.<br />
But before we do that, let&#8217;s add the script above as a file under our source code for the chat program. Make sure you change your name and repo in line 11. This file goes in the root of the chitchat directory.</p>
<pre class="brush: bash; collapse: true; highlight: [11]; light: false; title: ; toolbar: true; notranslate">
cat &lt;&lt; EOF &gt; Jenkinsfile
pipeline {
    agent any

    tools {nodejs &quot;node&quot;}

    stages {

        stage('Cloning Git') {
            steps {
                git 'https://github.com/&lt;your_name&gt;/&lt;your_repo&gt;'
            }
        }

        stage('Install dependencies') {
            steps {
                sh 'npm install'
            }
        }

        stage('Test') {
            steps {
                sh 'npm test'
            }
        }
    }
}
EOF
</pre>
<p>Add this file to Github.</p>
<pre class="brush: bash; title: ; notranslate">
git add .
git commit -m &quot;Added Jenkinsfile&quot;
git push -u origin master
</pre>
<h1>GitHub integration with Jenkins</h1>
<p>Log in to Github and from the upper right corner, click on your avatar and click on <strong>Settings</strong>. Then on the menu on the left side, all the way to the bottom click on <strong>Developer settings</strong>. Click on <strong>Personal access tokens</strong> and click on <strong>Generate new token</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-07.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-07.png" alt="" width="792" height="208" class="aligncenter size-full wp-image-5940" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-07.png 792w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-07-300x79.png 300w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-07-768x202.png 768w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-07-585x154.png 585w" sizes="(max-width: 792px) 100vw, 792px" /></a><br />
Give a name to your token (I&#8217;ll name mine Jenkins Token) and click on <strong>admin:repo_hook</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-08.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-08.png" alt="" width="458" height="100" class="aligncenter size-full wp-image-5942" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-08.png 458w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-08-300x66.png 300w" sizes="(max-width: 458px) 100vw, 458px" /></a><br />
Click on <strong>Generate token</strong> after. You&#8217;ll get a hex token, click on the clipboard icon next to it. You&#8217;ll need this token for the Jenkins configuration.<br />
Go to Jenkins and click on <strong>Manage Jenkins</strong> on the left followed by <strong>Configure System</strong> in the middle. Scroll down to the Git section, click on <strong>Add GitHub</strong> server, type <strong>GitHub </strong>for <strong>Name </strong>and next to <strong>Credentials </strong>click on <strong>Add</strong>, then <strong>Jenkins</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-09.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-09.png" alt="" width="1515" height="382" class="aligncenter size-full wp-image-5943" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-09.png 1515w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-09-300x76.png 300w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-09-768x194.png 768w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-09-1024x258.png 1024w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-09-1170x295.png 1170w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-09-585x148.png 585w" sizes="(max-width: 1515px) 100vw, 1515px" /></a><br />
When this window shows up, change the <strong>Kind </strong>to <strong>Secret text</strong>. Paste the token under <strong>Secret </strong>and enter something for <strong>ID </strong>and <strong>Description</strong>. Click <strong>Add</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-10.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-10.png" alt="" width="627" height="505" class="aligncenter size-full wp-image-5944" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-10.png 627w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-10-300x242.png 300w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-10-585x471.png 585w" sizes="(max-width: 627px) 100vw, 627px" /></a><br />
Once back, select the credential that we just created and click on <strong>Test connection</strong> on the right. You should get a message that the connection is fine. Make sure that <strong>Manage hooks</strong> is checked. Click on <strong>Save </strong>at the bottom once everything is OK.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-11.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-11.jpg" alt="" width="1113" height="230" class="aligncenter size-full wp-image-5948" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-11.jpg 1113w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-11-300x62.jpg 300w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-11-768x159.jpg 768w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-11-1024x212.jpg 1024w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-11-585x121.jpg 585w" sizes="(max-width: 1113px) 100vw, 1113px" /></a><br />
Go back to your project in Jenkins and click <strong>Configure </strong>on the left. Scroll down where the <strong>Pipeline </strong>is and change the <strong>Definition </strong>from <strong>Pipeline </strong>script to <strong>Pipeline script from SCM</strong>. SCM means Source Code Management. Change <strong>SCM </strong>to <strong>Git</strong>, enter the repo URL under <strong>Repositories </strong>. If your repo is not public, you&#8217;ll have to specify some credentials under <strong>Credentials</strong>. If your repo is public, you can leave them as <strong>none</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-12.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-12.png" alt="" width="1395" height="413" class="aligncenter size-full wp-image-5950" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-12.png 1395w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-12-300x89.png 300w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-12-768x227.png 768w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-12-1024x303.png 1024w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-12-1170x346.png 1170w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-12-585x173.png 585w" sizes="(max-width: 1395px) 100vw, 1395px" /></a><br />
Then scroll up a bit and check <strong>GitHub hook trigger for GitScm polling</strong> and then click <strong>Save</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-13.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-13.png" alt="" width="420" height="306" class="aligncenter size-full wp-image-5952" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-13.png 420w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-13-300x219.png 300w" sizes="(max-width: 420px) 100vw, 420px" /></a><br />
If you go back to GitHub, click on your project and all the way on the right click on <strong>Settings</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-14.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-14.png" alt="" width="812" height="114" class="aligncenter size-full wp-image-5953" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-14.png 812w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-14-300x42.png 300w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-14-768x108.png 768w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-14-585x82.png 585w" sizes="(max-width: 812px) 100vw, 812px" /></a><br />
Then click on <strong>Webhooks </strong>and you&#8217;ll see a green checkmark next to your Jenkins URL.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/10/P138-15.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/10/P138-15.png" alt="" width="1007" height="283" class="aligncenter size-full wp-image-5954" srcset="https://blog.andreev.it/wp-content/uploads/2019/10/P138-15.png 1007w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-15-300x84.png 300w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-15-768x216.png 768w, https://blog.andreev.it/wp-content/uploads/2019/10/P138-15-585x164.png 585w" sizes="(max-width: 1007px) 100vw, 1007px" /></a><br />
Go to your workstation and edit the index.html file. Change the line 4 for example so instead of Chit Chat App, replace the blank with a hyphen. e.g. Chit Chat to Chit-Chat. Save it and push to GitHib.</p>
<pre class="brush: bash; title: ; notranslate">
git add .
git commit -m &quot;Minor change in index.html&quot;
git push -u origin master
</pre>
<p>If you go back to Jenkins, you&#8217;ll see that there is a build there triggered automatically. This part is <strong>CI (Continous Integration)</strong>. But, let&#8217;s go further and expand our CI with creating a Docker image.</p>
<h1>Docker image and Docker Hub</h1>
<p>In order to create a Docker image, we need a <strong>Dockerfile</strong>. I won&#8217;t explain the meaning of each line in the Dockerfile, so just copy &#038; paste the following snippet in the chitchat directory.</p>
<pre class="brush: bash; title: ; notranslate">
cat &lt;&lt; EOF &gt; Dockerfile
FROM node:10
WORKDIR /usr/src/app
COPY package*.json ./

RUN npm install
COPY . .

EXPOSE 4141
CMD &#x5B; &quot;npm&quot;, &quot;start&quot; ]
EOF
</pre>
<p>This file will tell Docker exactly what to do when creating the image. But, we also have to tell Jenkins that we need to build the image, test the image and deploy it to Docker Hub. So, your new Jenkinsfile will look like this. Change the URLs in lines 6 and 7 to suit your needs. Before you deploy the script, go to Jenkins, click on <strong>Credentials </strong>on the left, click on <strong>System </strong>below, click on <strong>Global credentials (unrestricted)</strong> in the middle and then <strong>Add Credentials</strong> on the left. Fill out the form with your Docker Hub credentials. Whatever you put as ID for these username/password pair is what you have to put in line 8 in the script.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/11/P138-21.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/11/P138-21.jpg" alt="" width="524" height="369" class="aligncenter size-full wp-image-5999" srcset="https://blog.andreev.it/wp-content/uploads/2019/11/P138-21.jpg 524w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-21-300x211.jpg 300w" sizes="(max-width: 524px) 100vw, 524px" /></a><br />
And, now copy &#038; paste the script in the chitchat directory. If everything goes well, you&#8217;ll see your image deployed in Docker Hub and tagged as whatever your latest build was and the tag latest too.</p>
<pre class="brush: bash; highlight: [6,7]; title: ; notranslate">
cat &lt;&lt; 'EOF' &gt; Jenkinsfile
pipeline {

    environment {
        dockerregistry = 'https://registry.hub.docker.com'
        dockerhuburl = &quot;klimenta/chitchat&quot;
        githuburl = &quot;klimenta/chitchat&quot;
        dockerhubcrd = 'dockerhub'
    }

    agent any

    tools {nodejs &quot;node&quot;}

    stages {

        stage('Clone git repo') {
            steps {
                git 'https://github.com/' + githuburl
            }
        }

        stage('Install Node.js dependencies') {
            steps {
                sh 'npm install'
            }
        }

        stage('Test App') {
            steps {
                sh 'npm test'
            }
        }

        stage('Build image') {
          steps{
            script {
              dockerImage = docker.build(dockerhuburl + &quot;:$BUILD_NUMBER&quot;)
            }
          }
        }

        stage('Test image') {
            steps {
                sh 'docker run -i ' + dockerhuburl + ':$BUILD_NUMBER npm test'
            }
        }

        stage('Deploy image') {
          steps{
            script {
              docker.withRegistry(dockerregistry, dockerhubcrd ) {
                dockerImage.push(&quot;${env.BUILD_NUMBER}&quot;)
                dockerImage.push(&quot;latest&quot;)
              }
            }
          }
        }

        stage('Remove image') {
          steps{
            sh &quot;docker rmi $dockerhuburl:$BUILD_NUMBER&quot;
          }
        }
    }
}
EOF
</pre>
<p>Of course, you&#8217;ll have to tell GitHub that there is a change is a file.</p>
<pre class="brush: bash; title: ; notranslate">
git add .
git commit -m &quot;Added Jenkinsfile for Docker&quot;
git push -u origin master
</pre>
<h1>Deployment to Kubernetes using Jenkins</h1>
<p>Now that we are finished with the CI (Continuous Integration), it&#8217;s time for the CD (Continuous Deployment). We&#8217;ll use the existing Docker image that we put on Docker Hub. First thing first, we&#8217;ll have to install a plugin for a Kubernetes integration. Go to <strong>Manage Jenkins</strong> on the left, then click on <strong>Manage plugins</strong> in the center and click on <strong>Available </strong>tab. Install the <strong>Kubernetes Continuous Deploy</strong> plugin.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/11/P138-22.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/11/P138-22.jpg" alt="" width="510" height="71" class="aligncenter size-full wp-image-6003" srcset="https://blog.andreev.it/wp-content/uploads/2019/11/P138-22.jpg 510w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-22-300x42.jpg 300w" sizes="(max-width: 510px) 100vw, 510px" /></a><br />
Once installed and Jenkins is restarted, click on <strong>Credentials </strong>on the left, then <strong>System </strong>just below and then click on <strong>Global credentials (unrestricted)</strong> in the middle. Click on <strong>Add Credentials</strong> on the left. When this window pops up, change the <strong>Kind </strong>to <strong>Kubernetes configuration (kubeconfig)</strong>, enter an <strong>ID</strong>, in my case it&#8217;s k8s. Remember the <strong>ID</strong>, you&#8217;ll reference it in the <strong>Jenkinsfile </strong>later, type a description and select <strong>Enter directly</strong>. Before you click OK, we&#8217;ll have to go to our Kubernetes cluster and log to it.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/11/P138-23.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/11/P138-23.jpg" alt="" width="627" height="509" class="aligncenter size-full wp-image-6004" srcset="https://blog.andreev.it/wp-content/uploads/2019/11/P138-23.jpg 627w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-23-300x244.jpg 300w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-23-585x475.jpg 585w" sizes="(max-width: 627px) 100vw, 627px" /></a><br />
Once you are logged to your master node, log as the user that you use to manage the k8s cluster and run this command.</p>
<pre class="brush: bash; title: ; notranslate">
cat .kube/config
</pre>
<p>You&#8217;ll get a long list of text. Paste this back in the Jenkins window under <strong>Content</strong> and click OK. This is how Jenkins will communicate with your Kubernetes cluster. We also need to tell Kubernetes how we want our app deployed. As you know this is done with a YAML file. So, we have to add that YAML file under the chitchat directory.Change the IP in line 25 to match your master k8s node IP. I am deploying a load balancer that will listen on port 8080 and 2 pods in a deployment.</p>
<pre class="brush: xml; title: ; notranslate">
cat &lt;&lt; 'EOF' &gt; k8s.yaml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: chitchat
spec:
  replicas: 2
  template:
    metadata:
      name: chitchat
      labels:
        app: chitchat
    spec:
      containers:
      - image: $dockerhuburl:$BUILD_NUMBER
        name: nodejs
---
apiVersion: v1
kind: Service
metadata:
  name: loadbalancer
spec:
  type: LoadBalancer
  externalIPs:
  - 192.168.1.7
  selector:
    app: chitchat
  ports:
  - port: 8080
    targetPort: 4141
EOF
</pre>
<p>And we also want to tell Jenkins that we are adding an extra step to deploy it to the cluster, so the final Jenkins file looks like this.</p>
<pre class="brush: bash; title: ; notranslate">
cat &lt;&lt; 'EOF' &gt; Jenkinsfile
pipeline {

  environment {
    dockerregistry = 'https://registry.hub.docker.com'
    dockerhuburl = 'klimenta/chitchat'
    githuburl = 'klimenta/chitchat'
    dockerhubcrd = 'dockerhub'
    dockerImage = ''
  }

  agent any

  tools {nodejs &quot;node&quot;}

  stages {

    stage('Clone git repo') {
      steps {
         git 'https://github.com/' + githuburl
      }
    }

    stage('Install Node.js dependencies') {
      steps {
        sh 'npm install'
      }
    }

    stage('Test App') {
        steps {
            sh 'npm test'
        }
    }

    stage('Build image') {
      steps{
        script {
          dockerImage = docker.build(dockerhuburl + &quot;:$BUILD_NUMBER&quot;)
        }
      }
    }

    stage('Test image') {
      steps {
        sh 'docker run -i ' + dockerhuburl + ':$BUILD_NUMBER npm test'
      }
    }

    stage('Deploy image') {
      steps{
        script {
          docker.withRegistry(dockerregistry, dockerhubcrd ) {
            dockerImage.push(&quot;${env.BUILD_NUMBER}&quot;)
            dockerImage.push(&quot;latest&quot;)
          }
        }
      }
    }

    stage('Remove image') {
      steps{
        sh &quot;docker rmi $dockerhuburl:$BUILD_NUMBER&quot;
      }
    }

    stage('Deploy k8s') {
      steps {
        kubernetesDeploy(
          kubeconfigId: 'k8s',
          configs: 'k8s.yaml',
          enableConfigSubstitution: true
        )
      }
    }
  }
}
EOF
</pre>
<p>Trigger the change.</p>
<pre class="brush: bash; title: ; notranslate">
git add .
git commit -m &quot;Added Jenkinsfile for kubernetes and yaml&quot;
git push -u origin master
</pre>
<p>After a minute or so, you&#8217;ll see your pods running and if you go to your master node on port 8080 you&#8217;ll see the app happily running.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/11/P138-24.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/11/P138-24.jpg" alt="" width="424" height="344" class="aligncenter size-full wp-image-6025" srcset="https://blog.andreev.it/wp-content/uploads/2019/11/P138-24.jpg 424w, https://blog.andreev.it/wp-content/uploads/2019/11/P138-24-300x243.jpg 300w" sizes="(max-width: 424px) 100vw, 424px" /></a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2019/11/devops-ci-cd-using-git-jenkins-docker-kubernetes/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
			</item>
		<item>
		<title>CentOS: Kubernetes, Flannel and Calico in a single master configuration</title>
		<link>https://blog.andreev.it/2019/04/centos-kubernetes-flannel-and-calico-in-a-single-master-configuration/</link>
					<comments>https://blog.andreev.it/2019/04/centos-kubernetes-flannel-and-calico-in-a-single-master-configuration/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Tue, 09 Apr 2019 15:46:01 +0000</pubDate>
				<category><![CDATA[Docker]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Calico]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[Flannel]]></category>
		<guid isPermaLink="false">https://blog.andreev.it/?p=5127</guid>

					<description><![CDATA[Kubernetes (k8s) is getting a lot of attention and it&#8217;s becoming more and more&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>Kubernetes (k8s) is getting a lot of attention and it&#8217;s becoming more and more popular even in enterprises that are quite IT conservative. In this post I&#8217;ll explain how to install Kubernetes on a single master and one node on CentOS. Then, we&#8217;ll install the networking plugins (CNI), Flannel or Calico. At the end I&#8217;ll show an example of how to deploy a simple Node.js app and do rollout update and undoing the rollout. </p>
<div style="border:1px solid red; padding:16px;">
<p style="text-align:center;"><strong><span style="color:#800000;">NOTE ABOUT VERSIONS</span> </strong></p>
<p><center>Kubernetes and the surrounding components are changed on a daily basis. What works today, might not work tomorrow.</center></p>
<p><center>This tutorial assumes that you use CentOS 7, Docker 18.x and Kubernetes 1.14.</center></p>
</div>
<p>For this post, there are some pre-requisites. You will need 2 servers with 2 CPUs and at least 2GB RAM. It is also recommended to have a working DNS. If you don&#8217;t have DNS in your lab, make sure you use <strong>/etc/hosts</strong> for hostname resolution, but you can get away if you use IPs only (not recommended). </p>
<h1>Pre-requisites</h1>
<p>On a fresh installed CentOS 7, do these pre-req commands on both the master and the node at the same time. You need to be logged as root.<br />
Make sure SELinux is disabled.</p>
<pre class="brush: bash; title: ; notranslate">
setenforce 0
sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config
</pre>
<p>Kubernetes doesn&#8217;t like swap, so if you have it in <strong>/etc/fstab</strong>, disable the swap.</p>
<pre class="brush: bash; title: ; notranslate">
swapoff -a
sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab
</pre>
<p>Enable the bridge network module.</p>
<pre class="brush: bash; title: ; notranslate">
modprobe br_netfilter
echo '1' &gt; /proc/sys/net/bridge/bridge-nf-call-iptables
echo '1' &gt; /proc/sys/net/bridge/bridge-nf-call-ip6tables
</pre>
<p>Install Docker and change the cgroup from cfsgroup to systemd.</p>
<pre class="brush: bash; title: ; notranslate">
yum -y install yum-utils device-mapper-persistent-data lvm2
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum -y install docker-ce
mkdir /etc/docker
cat &lt;&lt;EOF &gt; /etc/docker/daemon.json
{
  &quot;exec-opts&quot;: &#x5B;&quot;native.cgroupdriver=systemd&quot;],
  &quot;log-driver&quot;: &quot;json-file&quot;,
  &quot;log-opts&quot;: {
    &quot;max-size&quot;: &quot;100m&quot;
  },
  &quot;storage-driver&quot;: &quot;overlay2&quot;,
  &quot;storage-opts&quot;: &#x5B;
    &quot;overlay2.override_kernel_check=true&quot;
  ]
}
EOF
mkdir -p /etc/systemd/system/docker.service.d
systemctl daemon-reload
systemctl enable docker &amp;&amp; systemctl start docker
</pre>
<p>Try this line and make sure the output says <strong>systemd</strong>.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
docker info | grep -i cgroup
Cgroup Driver: systemd
</pre>
<p>Add the Kubernetes repo. </p>
<pre class="brush: bash; title: ; notranslate">
cat &lt;&lt;EOF &gt; /etc/yum.repos.d/kubernetes.repo
&#x5B;kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
EOF
</pre>
<h1>Master Node</h1>
<p>Open the firewall <a href="https://kubernetes.io/docs/setup/independent/install-kubeadm/#check-required-ports" rel="noopener noreferrer" target="_blank">ports</a>. </p>
<pre class="brush: bash; title: ; notranslate">
firewall-cmd --add-port=6443/tcp --permanent
firewall-cmd --add-port=2379-2380/tcp --permanent
firewall-cmd --add-port=10250-10252/tcp --permanent
firewall-cmd --reload
</pre>
<p>From the repo install kubelet, kubectl and kubeadm and make sure Kubernetes starts on boot.</p>
<pre class="brush: bash; title: ; notranslate">
yum -y install kubelet kubectl kubeadm
systemctl enable kubelet
</pre>
<p>Don&#8217;t start Kubernetes yet. It will fail with a message that it can&#8217;t find a config yaml file. Just initialize the cluster. This will also start the kubelet service. Pick one choice (Flannel or Calico).<br />
<strong>NOTE: This line initializes the cluster to be used for Flannel.</strong> </p>
<pre class="brush: bash; title: ; notranslate">
kubeadm init --pod-network-cidr=10.244.0.0/16
</pre>
<p><strong>NOTE: This line initializes the cluster to be used for Calico.</strong> </p>
<pre class="brush: bash; title: ; notranslate">
kubeadm init --pod-network-cidr=192.168.0.0/16
</pre>
<p>Look at the bottom of the output. You should see something like this. Lines 5,6,7 and 15 and 16 are important. </p>
<pre class="brush: plain; highlight: [5,6,7,15,16]; title: ; notranslate">
Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config

You should now deploy a pod network to the cluster.
Run &quot;kubectl apply -f &#x5B;podnetwork].yaml&quot; with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 192.168.1.175:6443 --token 0z3iov.28rz29hxw9ft4jmg \
    --discovery-token-ca-cert-hash sha256:782d37b5c870ebebd2f17cc3ba1424f305e6a3e293afc04fc2030edfab6bf4b0
</pre>
<p>This means that the cluster initialized OK.<br />
Check the status of both Docker and Kubernetes.</p>
<pre class="brush: bash; title: ; notranslate">
systemctl status docker | grep Active
systemctl status kubelet | grep Active
</pre>
<p>Make sure they are both running. Check <strong>/var/log/messages</strong> if you have any issues.</p>
<h1>Nodes (workers)</h1>
<p>On the worker nodes, make sure you do the same as you did on the master (swap, SELinux, Docker) except that you don&#8217;t have to install kubectl. </p>
<pre class="brush: bash; title: ; notranslate">
yum -y install kubelet kubeadm
systemctl enable kubelet
</pre>
<p>Open the firewall <a href="https://kubernetes.io/docs/setup/independent/install-kubeadm/#check-required-ports" rel="noopener noreferrer" target="_blank">ports</a>. </p>
<pre class="brush: bash; title: ; notranslate">
firewall-cmd --add-port=10250/tcp --permanent
firewall-cmd --add-port=30000-32767/tcp --permanent
firewall-cmd --reload
</pre>
<p>Now, you can join the cluster. Use the command that was the output from the <strong>kubeadm init</strong> on the master (see above &#8211; lines 15 and 16).</p>
<pre class="brush: bash; title: ; notranslate">
kubeadm join 192.168.1.175:6443 --token 0z3iov.28rz29hxw9ft4jmg \
    --discovery-token-ca-cert-hash sha256:782d37b5c870ebebd2f17cc3ba1424f305e6a3e293afc04fc2030edfab6bf4b0
</pre>
<p>That&#8217;s how you join nodes to the master. Replace <strong>192.168.1.175</strong> with the IP or hostname of your master node. If everything is OK, you&#8217;ll see something like this.</p>
<pre class="brush: bash; title: ; notranslate">
This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.

Run 'kubectl get nodes' on the control-plane to see this node join the cluster.
</pre>
<h1>Kubernetes user</h1>
<p>While still logged as root on the master, create the Kubernetes user that you will use for managing the k8s cluster. In my case, I&#8217;ll create a user called k8s with <strong>secret </strong>as password.</p>
<pre class="brush: bash; title: ; notranslate">
useradd k8s -g docker
usermod -aG wheel k8s
echo -e &quot;secret\nsecret&quot; | passwd k8s
</pre>
<p>Log as this user (k8s) and execute these commands. These lines were also an output of the <strong>kubeadm init</strong> command above (5,6 and 7). </p>
<pre class="brush: bash; title: ; notranslate">
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
</pre>
<p>If you want to add another user to manage the Kubernetes cluster, make sure you execute these 3 lines above for that user. Check if everything looks good.</p>
<pre class="brush: bash; title: ; notranslate">
docker ps
</pre>
<p>You should see a bunch of Kubernetes system containers running (etcd, scheduler, API server).<br />
Then check the nodes.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
kubectl get nodes
NAME                      STATUS     ROLES    AGE   VERSION
k8smaster.andreev.local   NotReady   master   12m   v1.14.0
k8snode1.andreev.local    NotReady   &lt;none&gt;   10m   v1.14.0
</pre>
<p>The reason the master and the node are not ready is because we don&#8217;t have a network for the cluster. </p>
<h1>Network CNI</h1>
<p>Depending on how you&#8217;ve initialized the cluster, pick one of the network plugins (Flannel or Calico). </p>
<h2>Flannel</h2>
<p>For the network to work, we&#8217;ll have to use one of the CNI plugins. There are many, Flannel, Weave Net, Calico etc.<br />
Let&#8217;s install Flannel. Do this on the master only logged as k8s user. The master will take care of the nodes.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
</pre>
<p>Start this little infinite loop and you&#8217;ll see that after 20-30 seconds, both the master and the node will change their status to <strong>Ready</strong>. Hit Ctrl-C to end.</p>
<pre class="brush: bash; title: ; notranslate">
while true
do
kubectl get nodes
sleep 3
done
</pre>
<p>Now, you have a fully working cluster ready. </p>
<h2>Calico</h2>
<p>For the network to work, we&#8217;ll have to use one of the CNI plugins. There are many, Flannel, Weave Net, Calico etc.<br />
Let&#8217;s install Flannel. Do this on the master only logged as k8s user. The master will take care of the nodes.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl apply -f https://docs.projectcalico.org/v3.3/getting-started/kubernetes/installation/hosted/rbac-kdd.yaml
kubectl apply -f https://docs.projectcalico.org/v3.3/getting-started/kubernetes/installation/hosted/kubernetes-datastore/calico-networking/1.7/calico.yaml
</pre>
<p>Start this little infinite loop and you&#8217;ll see that after 20-30 seconds, both the master and the node will change their status to <strong>Ready</strong>. Hit Ctrl-C to end.</p>
<pre class="brush: bash; title: ; notranslate">
while true
do
kubectl get nodes
sleep 3
done
</pre>
<p>Now, you have a fully working cluster ready. </p>
<h1>Deployment</h1>
<p>In this example, I&#8217;ll create a small container that runs a Node.js app that when run it will display &#8220;Hello from &#8221; the hostname of the container. On top of that we&#8217;ll create a load balancer, so we can see how that works.<br />
First, let&#8217;s create the container based on Node.js image. Create a file named <strong>Dockerfile </strong>with this content. </p>
<pre class="brush: bash; title: ; notranslate">
FROM node:latest
LABEL maintainer &quot;kliment@andreev.it&quot;
ADD appv1.js /app.js
ENTRYPOINT &#x5B;&quot;node&quot;, &quot;app.js&quot;]
</pre>
<p>This is our application. Save it as <strong>appv1.js</strong>.</p>
<pre class="brush: xml; title: ; notranslate">
const http = require('http');
const os = require('os');
const port = 3000;

const server = http.createServer((req, res) =&gt; {
  res.statusCode = 200;
  res.end('Hello from ' + os.hostname() + '\n');
});

server.listen(port);
</pre>
<p>Create the container. You&#8217;ll need a valid Docker Hub login. In my case, my username is klimenta. Replace it with yours.</p>
<pre class="brush: bash; title: ; notranslate">
docker build -t klimenta/appv1:latest .
</pre>
<p>It&#8217;s time to login to Docker Hub and upload the image there. You&#8217;ll be prompted for a username and password.</p>
<pre class="brush: bash; title: ; notranslate">
docker login
</pre>
<p>Upload the image.</p>
<pre class="brush: bash; title: ; notranslate">
docker push klimenta/appv1:latest
</pre>
<p>Create a Kubernetes deployment file named <strong>deployment.yaml</strong>.</p>
<pre class="brush: xml; title: ; notranslate">
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: appv1
spec:
  replicas: 3
  template:
    metadata:
      name: appv1
      labels:
        app: appv1
    spec:
      containers:
      - image: klimenta/appv1:latest
        name: nodejs
---
apiVersion: v1
kind: Service
metadata:
  name: loadbalancer
spec:
  type: LoadBalancer
  selector:
    app: appv1
  ports:
  - port: 80
    targetPort: 3000
</pre>
<p>We are creating a deployment with 3 replicas and a load balancer that listens on port 80 and sends the traffic to port 3000 on the pods with our application.<br />
Create the deployment and the load balanced service.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl create -f deployment.yaml
</pre>
<p>After about 30 seconds, you&#8217;ll see that your pods are ready. </p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
appv1-596dd64666-4k7qn   1/1     Running   0          93m
appv1-596dd64666-gn5gr   1/1     Running   0          93m
appv1-596dd64666-vv9h5   1/1     Running   0          93m
</pre>
<p>The load balancer is also ready. </p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
kubectl get svc
NAME           TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
kubernetes     ClusterIP      10.96.0.1       &lt;none&gt;        443/TCP        102m
loadbalancer   LoadBalancer   10.111.212.41   &lt;pending&gt;     80:30310/TCP   94m
</pre>
<p>If you hit the load balancer, you&#8217;ll see a response. Replace the IP with yours.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
curl http://10.111.212.41
Hello from appv1-596dd64666-gn5gr
</pre>
<p>Now, let&#8217;s say that we created a new version of our application. Copy appv1.js as appv2.js and change  <strong>appv2.js</strong> a little bit.</p>
<pre class="brush: bash; title: ; notranslate">
cp appv1.js appv2.js
</pre>
<p>The appv2.js should look like this.</p>
<pre class="brush: xml; highlight: [7]; title: ; notranslate">
const http = require('http');
const os = require('os');
const port = 3000;

const server = http.createServer((req, res) =&gt; {
  res.statusCode = 200;
  res.end('Greetings from ' + os.hostname() + '\n');
});

server.listen(port);
</pre>
<p>Change the <strong>Dockerfile </strong>to look like this.</p>
<pre class="brush: bash; title: ; notranslate">
FROM node:latest
LABEL maintainer &quot;kliment@andreev.it&quot;
ADD appv2.js /app.js
ENTRYPOINT &#x5B;&quot;node&quot;, &quot;app.js&quot;]
</pre>
<p>Build the new image and upload it to Docker Hub.</p>
<pre class="brush: bash; title: ; notranslate">
docker build -t klimenta/appv2:latest .
docker push klimenta/appv2:latest
</pre>
<p>Deploy the new application.</p>
<pre class="brush: bash; title: ; notranslate">
kubectl set image deployment appv1 nodejs=klimenta/appv2:latest
</pre>
<p>If you check the app now, you&#8217;ll see that it reflects the new version.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
curl http://10.111.212.41
Greetings from appv1-5d949774f5-b794k
</pre>
<p>But what if there is a bug in our application and we want to revert it back to the initial one? Easy.</p>
<pre class="brush: bash; highlight: [1,3]; title: ; notranslate">
kubectl rollout undo deployment appv1
deployment.extensions/appv1 rolled back
curl http://10.111.212.41
Hello from appv1-596dd64666-94gqh
</pre>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2019/04/centos-kubernetes-flannel-and-calico-in-a-single-master-configuration/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
