<?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>DevOps &#8211; Blog of Kliment Andreev &#8211; A place so I won&#039;t forget things</title>
	<atom:link href="https://blog.andreev.it/category/devops/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>Install Kubernetes with CRI-O and Cilium on Rocky Linux 9 / VMware / Bare Metal</title>
		<link>https://blog.andreev.it/2023/10/install-kubernetes-with-cri-o-and-cilium-on-rocky-linux-9/</link>
					<comments>https://blog.andreev.it/2023/10/install-kubernetes-with-cri-o-and-cilium-on-rocky-linux-9/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Mon, 09 Oct 2023 16:18:16 +0000</pubDate>
				<category><![CDATA[Containers]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[ESX/ESXi]]></category>
		<category><![CDATA[Rocky Linux]]></category>
		<category><![CDATA[Cilium]]></category>
		<category><![CDATA[CRI-O]]></category>
		<category><![CDATA[ESXi]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[MetalLB]]></category>
		<category><![CDATA[rocky linux]]></category>
		<category><![CDATA[VMware]]></category>
		<guid isPermaLink="false">https://blog.andreev.it/?p=9532</guid>

					<description><![CDATA[In this post, I&#8217;ll build a Kubernetes cluster with one master and two nodes&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>In this post, I&#8217;ll build a Kubernetes cluster with one master and two nodes using CRI-O and Cilium. I have DNS in my environment but you can use the hosts files if needed.<br />
The OS will be Rocky Linux 9 running on ESXi 8.x but this should work on any other bare metal server too. The config for the nodes is:</p>
<p>&#8211; Master: 2 CPUs, 4GB RAM<br />
&#8211; Workers: 2 CPUs, 8GB RAM.</p>
<p>First, we&#8217;ll have to disable swap and put SELinux in permissive mode.<br />
<strong>Do this on all nodes (master + workers).</strong></p>
<pre class="brush: bash; title: ; notranslate">
sudo swapoff -a
sudo sed -i &#039;/ swap / s/^\(.*\)$/#\1/g&#039; /etc/fstab
sudo setenforce 0
sudo sed -i &#039;s/^SELINUX=enforcing$/SELINUX=permissive/&#039; /etc/selinux/config
</pre>
<p>Now, we have to open some firewall ports for Kubernetes..<br />
<strong>Do this on the master node only.</strong></p>
<pre class="brush: bash; title: ; notranslate">
sudo firewall-cmd --permanent --add-port=6443/tcp
sudo firewall-cmd --permanent --add-port=2379-2380/tcp
sudo firewall-cmd --permanent --add-port=10250/tcp
sudo firewall-cmd --permanent --add-port=10259/tcp
sudo firewall-cmd --permanent --add-port=10257/tcp
sudo firewall-cmd --reload
</pre>
<p><strong>Do this on the workers only.</strong></p>
<pre class="brush: bash; title: ; notranslate">
sudo firewall-cmd --permanent --add-port=10250/tcp
sudo firewall-cmd --permanent --add-port=30000-32767/tcp   
sudo firewall-cmd --reload
</pre>
<p>In addition, we&#8217;ll have to open the firewall ports for Cilium.<br />
<strong>Do this on all nodes</strong>.</p>
<pre class="brush: bash; title: ; notranslate">
sudo firewall-cmd --permanent --add-port=4240/tcp 
sudo firewall-cmd --permanent --add-port=8472/udp 
sudo firewall-cmd --reload
</pre>
<p>Now, we have to make some changes that are also required. Some necessary modules and IPtables change.<br />
<strong>Do this on all nodes.</strong></p>
<pre class="brush: bash; title: ; notranslate">
cat &lt;&lt;EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

sudo modprobe overlay
sudo modprobe br_netfilter

# sysctl params required by setup, params persist across reboots
cat &lt;&lt;EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF

# Apply sysctl params without reboot
sudo sysctl --system
</pre>
<p>We have to install CRI-O container runtime.<br />
<strong>Do this on all nodes.</strong></p>
<pre class="brush: bash; title: ; notranslate">
VERSION=1.22
sudo curl -L -o /etc/yum.repos.d/devel:kubic:libcontainers:stable.repo https://download.opensuse.org/repositories/devel:kubic:libcontainers:stable/CentOS_8/devel:kubic:libcontainers:stable.repo
sudo curl -L -o /etc/yum.repos.d/devel:kubic:libcontainers:stable:cri-o:${VERSION}.repo https://download.opensuse.org/repositories/devel:kubic:libcontainers:stable:cri-o:${VERSION}/CentOS_8/devel:kubic:libcontainers:stable:cri-o:${VERSION}.repo
sudo dnf -y install cri-o cri-tools
sudo systemctl enable --now crio
sudo systemctl status crio
</pre>
<p>Then, we have to install Kubernetes.<br />
<strong>Do this on all nodes.</strong></p>
<pre class="brush: bash; title: ; notranslate">
cat &lt;&lt;EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
&#x5B;kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-\$basearch
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
exclude=kubelet kubeadm kubectl
EOF
sudo dnf install -y kubelet kubeadm kubectl --disableexcludes=kubernetes
sudo systemctl enable --now kubelet
</pre>
<p><strong>On the master node only</strong>, initialize the cluster. Replace <strong>master.homelab.com</strong> with your master IP if you don&#8217;t use DNS.</p>
<pre class="brush: bash; title: ; notranslate">
sudo kubeadm init --control-plane-endpoint master.homelab.local:6443
</pre>
<p>You&#8217;ll get an output that says how to join the nodes, something like kubeadm join master&#8230; + a token<br />
<strong>On the master node</strong>, add these lines.</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>Join the nodes to the cluster.<br />
<strong>Do this on workers only.</strong></p>
<pre class="brush: bash; title: ; notranslate">
sudo kubeadm join master.homelab.local:6443 --token t2yir0.7n49rn6r57msy4j4 \
        --discovery-token-ca-cert-hash sha256:563360fcf60c49be91cdcf6486a4954c579a80c54503127ee0682ab8f86ec840
</pre>
<p>Check the nodes.<br />
<strong>Do this on the master node.</strong></p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
kubectl get nodes
NAME                   STATUS   ROLES           AGE    VERSION
master.homelab.local   Ready    control-plane   116s   v1.28.2
node1.homelab.local    Ready    &lt;none&gt;          89s    v1.28.2
node2.homelab.local    Ready    &lt;none&gt;          85s    v1.28.2
</pre>
<p>&#8230;then check all the pods.<br />
<strong>Do this on the master node.</strong></p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
kubectl get pods --all-namespaces
NAMESPACE     NAME                                           READY   STATUS    RESTARTS   AGE
kube-system   coredns-5dd5756b68-5ljgg                       1/1     Running   0          2m
kube-system   coredns-5dd5756b68-jp8tq                       1/1     Running   0          2m
kube-system   etcd-master.homelab.local                      1/1     Running   0          2m6s
kube-system   kube-apiserver-master.homelab.local            1/1     Running   0          2m6s
kube-system   kube-controller-manager-master.homelab.local   1/1     Running   0          2m6s
kube-system   kube-proxy-2z64r                               1/1     Running   0          2m
kube-system   kube-proxy-mrwjv                               1/1     Running   0          96s
kube-system   kube-proxy-nj5q4                               1/1     Running   0          100s
kube-system   kube-scheduler-master.homelab.local            1/1     Running   0          2m6s
</pre>
<p>Install Cilium<strong> on the master node only.</strong></p>
<pre class="brush: bash; title: ; notranslate">
CILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)
CLI_ARCH=amd64
if &#x5B; &quot;$(uname -m)&quot; = &quot;aarch64&quot; ]; then CLI_ARCH=arm64; fi
curl -L --fail --remote-name-all https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-${CLI_ARCH}.tar.gz{,.sha256sum}
sha256sum --check cilium-linux-${CLI_ARCH}.tar.gz.sha256sum
sudo tar xzvfC cilium-linux-${CLI_ARCH}.tar.gz /usr/local/bin
rm cilium-linux-${CLI_ARCH}.tar.gz{,.sha256sum}
cilium install --version 1.14.2
</pre>
<p>Check the status.</p>
<pre class="brush: bash; title: ; notranslate">
cilium status --wait
</pre>
<p>You should see something like this. </p>
<pre class="brush: bash; title: ; notranslate">
    /¯¯\
 /¯¯\__/¯¯\    Cilium:             OK
 \__/¯¯\__/    Operator:           OK
 /¯¯\__/¯¯\    Envoy DaemonSet:    disabled (using embedded mode)
 \__/¯¯\__/    Hubble Relay:       disabled
    \__/       ClusterMesh:        disabled

DaemonSet              cilium             Desired: 3, Ready: 3/3, Available: 3/3
Deployment             cilium-operator    Desired: 1, Ready: 1/1, Available: 1/1
Containers:            cilium             Running: 3
                       cilium-operator    Running: 1
Cluster Pods:          2/2 managed by Cilium
Helm chart version:    1.14.2
Image versions         cilium             quay.io/cilium/cilium:v1.14.2@sha256:6263f3a3d5d63b267b538298dbeb5ae87da3efacf09a2c620446c873ba807d35: 3
                       cilium-operator    quay.io/cilium/operator-generic:v1.14.2@sha256:52f70250dea22e506959439a7c4ea31b10fe8375db62f5c27ab746e3a2af866d: 1
</pre>
<p>Then you can run the connectivity test. Some of the egress test will fail because we don&#8217;t have a public IP on the cluster.</p>
<pre class="brush: bash; title: ; notranslate">
cilium connectivity test
</pre>
<p>You should be able to run your deployments now. If you need to access the pods using URL use something like NodePort or port-forwarding. It&#8217;s very cumbersome and inefficient, but you can use a load balancer with MetalLB. Read <a href="https://blog.andreev.it/2023/10/install-metallb-on-kubernetes-cluster-running-on-vmware-vms-or-bare-metal-server/" rel="noopener" target="_blank">this</a> post to see how to do that. It&#8217;s separated from this post, because it&#8217;s a separate topic than just k8s, container runtime and CNI.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2023/10/install-kubernetes-with-cri-o-and-cilium-on-rocky-linux-9/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>Ansible: Quick Start Guide for FreeBSD, CentOS and Ubuntu</title>
		<link>https://blog.andreev.it/2021/02/ansible-quick-start-guide-for-freebsd-centos-and-ubuntu/</link>
					<comments>https://blog.andreev.it/2021/02/ansible-quick-start-guide-for-freebsd-centos-and-ubuntu/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Mon, 08 Feb 2021 17:04:37 +0000</pubDate>
				<category><![CDATA[CentOS]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[FreeBSD]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Ubuntu]]></category>
		<category><![CDATA[Ansible]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[freebsd]]></category>
		<guid isPermaLink="false">https://blog.andreev.it/?p=8784</guid>

					<description><![CDATA[In this post/howto, I&#8217;ll explain how to install Ansible as control and managed node&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>In this post/howto, I&#8217;ll explain how to install Ansible as control and managed node on FreeBSD 12, CentOS 8 and Ubuntu 18. Then, I&#8217;ll explain how to create SSH keys so the nodes can communicate and some basic tasks. Then, I&#8217;ll show you how to create a playbook to install the latest updates and also install an Apache server with the default settings. Finally, I&#8217;ll show an example of how to use variables. </p>
<h1>Control and managed nodes</h1>
<p>A control node is where you execute all of your Ansible commands and eventually keep your playbooks, configs, inventory etc. It&#8217;s pretty much your workstation. A managed node is where the actual playbooks are executed. <a href="https://docs.ansible.com/ansible/latest/network/getting_started/basic_concepts.html" rel="noopener" target="_blank">These </a>are the main concepts and the terminology.<br />
In my case, I have 4 VMs/instances. The main one which is the control node and 3 managed nodes.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2021/01/P153-01.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2021/01/P153-01.png" alt="" width="309" height="113" class="aligncenter size-full wp-image-8786" srcset="https://blog.andreev.it/wp-content/uploads/2021/01/P153-01.png 309w, https://blog.andreev.it/wp-content/uploads/2021/01/P153-01-300x110.png 300w" sizes="(max-width: 309px) 100vw, 309px" /></a></p>
<h2>CentOS 8</h2>
<p>If you want to install Ansible on the control node, you have to install Python 3.x first. </p>
<pre class="brush: bash; title: ; notranslate">
sudo dnf install python3
</pre>
<p>This will also install <strong>pip</strong>. Type <strong>python3 </strong>to test, <strong>CTRL-D</strong> to exit and then install ansible.</p>
<pre class="brush: bash; title: ; notranslate">
sudo pip3 install ansible
</pre>
<p>Type <strong>ansible </strong>to test.</p>
<h2>Ubuntu</h2>
<p>Ubuntu comes with python installed, but not with pip. Install pip with:</p>
<pre class="brush: bash; title: ; notranslate">
sudo apt install python3-pip
</pre>
<p>Then install ansible with:</p>
<pre class="brush: bash; title: ; notranslate">
sudo pip3 install ansible
</pre>
<p>Type <strong>ansible </strong>to test.</p>
<h2>FreeBSD</h2>
<p>FreeBSD comes with python installed but pip is not. Check the version and then install the same pip version. </p>
<pre class="brush: bash; title: ; notranslate">
ls -l /usr/local/bin/python*
</pre>
<p>If your output is for example <strong>python37</strong>, install the same pip version.</p>
<pre class="brush: bash; title: ; notranslate">
pkg install py37-pip
</pre>
<p>Then install ansible.</p>
<pre class="brush: bash; title: ; notranslate">
pip install ansible
</pre>
<p>Type <strong>ansible </strong>to test.</p>
<h1>SSH keys</h1>
<p>While Ansible can use standard *nix username/password authentication, it&#8217;s recommended that you use SSH keys to communicate from control node to the managed nodes. For that, you&#8217;ll have to create your SSH keys. Let&#8217;s say you have an account on your control node and the username is admin. You also want to use the user ansible on the managed nodes. It really doesn&#8217;t matter what usernames you are going to choose. You can always override the keys to use, but in this case, I&#8217;ll create a key on the control node and send it to all managed nodes.<br />
On the control node, regardless of your OS, do:</p>
<pre class="brush: bash; title: ; notranslate">
ssh-keygen -b 4096
</pre>
<p>This will create a subfolder <strong>.ssh</strong> with two files: <strong>id_rsa</strong> and <strong>id_rsa.pub</strong>. The former is your private key and the later is your public key.<br />
Copy the key to your managed nodes.</p>
<pre class="brush: bash; title: ; notranslate">
ssh-copy-id ansible@nodeX.andreev.local
</pre>
<p>This will copy my key for the user admin to the node X under the ansible user. Then test the passwordless connection.</p>
<pre class="brush: bash; title: ; notranslate">
ssh ansible@nodeX.andreev.local
</pre>
<p>Mind that the use of FQDN (nodex.andreev.local) vs. hostname (nodex) is important. For SSH these two are different. Once you log to the managed node, the node will be added to the list of known hosts in the file <strong>.ssh/known_hosts</strong>. </p>
<h1>Inventory and the config file</h1>
<p>Ansible uses the inventory files to execute an action against using the options and parameters specified in the config file. The config file is <strong>/etc/ansible/ansible.cfg</strong> for CentOS and Ubuntu and <strong>/usr/local/etc/ansible/ansible.cfg </strong>for FreeBSD. You can also put the inventory file in the same directory and name it as you wish, but you have to specify the inventory as a parameter on the command line or an entry in the config file. In addition, you can have your config file in your current directory or under the <strong>.ansible</strong> directory in your home folder. <strong>ansible.cfg</strong> in the current directory has a precedence over <strong>.ansible.cfg</strong> in the home directory which has a precedence over <strong>/etc/ansible/ansible.cfg</strong>. Here is how that looks.<br />
Let&#8217;s list all the inventory.</p>
<pre class="brush: bash; title: ; notranslate">
ansible --list-hosts all
</pre>
<p>You&#8217;ll get a message that there is no inventory file. Let&#8217;s create one, we&#8217;ll name it <strong>inventory.txt</strong>.</p>
<pre class="brush: bash; title: ; notranslate">
&#x5B;freebsd]
node1.andreev.local
&#x5B;centos]
node2.andreev.local
&#x5B;ubuntu]
node3.andreev.local
&#x5B;bsd]
node1.andreev.local
&#x5B;linux]
node2.andreev.local
node3.andreev.local
</pre>
<p>If we specify the inventory file, we&#8217;ll get this.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
ansible --list-hosts all -i inventory.txt
  hosts (3):
    node1.andreev.local
    node2.andreev.local
    node3.andreev.local
</pre>
<p>If we create a config file, we can tell ansible where to look for the inventory. Create a file <strong>ansible.cfg</strong> in the same directory.</p>
<pre class="brush: bash; title: ; notranslate">
&#x5B;defaults]
inventory=/home/&lt;somewhere&gt;/inventory.txt
</pre>
<p>If you do <strong>ansible &#8211;list-hosts all</strong> now, you&#8217;ll get the same result as before, but without specifying the inventory file.<br />
Or something like this. </p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
ansible all -m shell -a &quot;uname -a&quot;
node1.andreev.local | CHANGED | rc=0 &gt;&gt;
FreeBSD node1.andreev.local 12.1-RELEASE FreeBSD 12.1-RELEASE r354233 GENERIC  amd64
node2.andreev.local | CHANGED | rc=0 &gt;&gt;
Linux node2.andreev.local 4.18.0-193.14.2.el8_2.x86_64 #1 SMP Sun Jul 26 03:54:29 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
node3.andreev.local | CHANGED | rc=0 &gt;&gt;
Linux node3.andreev.local 4.15.0-129-generic #132-Ubuntu SMP Thu Dec 10 14:02:26 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
</pre>
<p>By default, ansible runs on the managed node with the currently logged user that executes the playbook on the control node. If you get an error saying that the previous command cannot connect to the host, you have to specify the same user that you used when you test the connection with <em>ssh -user-@managednode.</em> So, edit <strong>ansible.cfg</strong> and add this line.</p>
<pre class="brush: bash; title: ; notranslate">
remote_user=&lt;user&gt;
</pre>
<p>We can target only the group linux which consists of Linux hosts only in the inventory file. </p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
ansible linux -m shell -a &quot;date&quot;
node3.andreev.local | CHANGED | rc=0 &gt;&gt;
Fri Jan  8 15:23:40 UTC 2021
node2.andreev.local | CHANGED | rc=0 &gt;&gt;
Fri Jan  8 10:23:40 EST 2021
</pre>
<h1>Playbooks</h1>
<p>The playbooks are the blueprints of the automation tasks. Instead of running the ansible command to execute each task separately, we combine these tasks in a YAML file and execute them sequentially. Here are 3 playbooks that update each of our managed nodes. There is no update module for FreeBSD, so we use the shell command.</p>
<pre class="brush: yaml; title: ; notranslate">
# freebsd-update.yml
---
  - hosts: freebsd
    become: yes
    tasks:
      - name: Fetch all packages
        shell: freebsd-update fetch
      - name: Install FreeBSD updates
        shell: freebsd-update install
      - name: Reboot
        reboot:
</pre>
<p>For CentOS we&#8217;ll use <strong>yum</strong>. </p>
<pre class="brush: yaml; title: ; notranslate">
# centos-update.yml
---
  - hosts: centos
    become: yes
    tasks:
      - name: Update all packages
        yum: name=* state=latest
      - name: Reboot
        reboot:
</pre>
<p>&#8230;and for Ubuntu we&#8217;ll use <strong>apt</strong>.</p>
<pre class="brush: yaml; title: ; notranslate">
# ubuntu-update.yml
---
  - hosts: ubuntu
    become: yes
    tasks:
      - name: Update all packages
        apt: name=* state=latest
      - name: Reboot
        reboot:
</pre>
<p>Save these files with a YAML extension and you can execute them with the following command.</p>
<pre class="brush: bash; title: ; notranslate">
ansible-playbook &lt;filename&gt;
</pre>
<p>All of them will probably fail. That&#8217;s because your ansible user on the managed nodes is required a password when executing a <strong>sudo </strong>command. In order to fix that, you&#8217;ll have to add a line in the sudoers file. Edit this file using the <strong>visudo </strong>command.</p>
<pre class="brush: bash; title: ; notranslate">
visudo
</pre>
<p>&#8230;and then add this line right before the <strong>@includedir <...></strong> which is the last line in the file.</p>
<pre class="brush: bash; title: ; notranslate">
ansible ALL=(ALL) NOPASSWD:ALL
</pre>
<p>Where <strong>ansible </strong>is the user that runs the playbooks on the managed nodes. FreeBSD doesn&#8217;t come with sudo preinstalled, so you&#8217;ll have to install it first on the managed node.</p>
<pre class="brush: bash; title: ; notranslate">
pkg install sudo
</pre>
<p>These playbooks will update the OS and the packages for the Linux. For FreeBSD, it will update only the OS. Here is another example of playbooks that will install Apache server in a default configuration and change the <strong>ServerName </strong>and <strong>ServerAdmin </strong>lines. We&#8217;ll also install PHP and test our server.<br />
If you have a firewall enabled, make sure you open it up first on CentOS. Ubuntu and FreeBSD do not come with the firewall enabled. </p>
<pre class="brush: bash; title: ; notranslate">
firewall-cmd --zone=public --permanent --add-service=http
firewall-cmd --reload
</pre>
<p>For FreeBSD, the playbook looks like this. </p>
<pre class="brush: yaml; title: ; notranslate">
# freebsd-apache.yml
---
  - hosts: freebsd
    become: yes
    tasks:
      - name: Install apache and php
        pkgng:
          name:
            - apache24
            - php74
            - mod_php74
          state:  present
      - name: Start on reboot
        service: name=apache24 enabled=yes
      - name: Copy index.php
        copy:
          src: ../files/index.php
          dest: /usr/local/www/apache24/data
          mode: 0755
      - name: Copy mod_php.conf
        copy:
          src: ../files/mod_php.conf
          dest: /usr/local/etc/apache24/modules.d
          mode: 0755
      - name: Start apache now
        service: name=apache24 state=started
</pre>
<p>For CentOS, it looks like this.</p>
<pre class="brush: yaml; title: ; notranslate">
# centos-apache.yml
---
  - hosts: centos
    become: yes
    tasks:
      - name: Install apache and php
        yum:
          name:
            - httpd
            - php
          state:  present
      - name: Start apache now and on reboot
        service: name=httpd state=started enabled=yes
      - name: Copy index.php
        copy:
          src: ../files/index.php
          dest: /var/www/html
          mode: 0755
</pre>
<p>&#8230;and for Ubuntu it looks like this.</p>
<pre class="brush: yaml; title: ; notranslate">
# ubuntu-apache.yml
---
  - hosts: ubuntu
    become: yes
    tasks:
      - name: Install apache and php
        apt:
          name:
            - apache2
            - php
          state:  present
      - name: Start apache now and on reboot
        service: name=apache2 state=started enabled=yes
      - name: Copy index.php
        copy:
          src: ../files/index.php
          dest: /var/www/html
          mode: 0755
</pre>
<p>You will also need these two files in a directory called <strong>files</strong>. In my case it&#8217;s one level above the directory where I keep my playbooks.<br />
<strong>index.php</strong></p>
<pre class="brush: php; title: ; notranslate">
&lt;?php
  phpinfo();
?&gt;
</pre>
<p><strong>001_mod-php.conf</strong></p>
<pre class="brush: bash; title: ; notranslate">
&lt;IfModule dir_module&gt;
    DirectoryIndex index.php index.html
    &lt;FilesMatch &quot;\.php$&quot;&gt;
        SetHandler application/x-httpd-php
    &lt;/FilesMatch&gt;
    &lt;FilesMatch &quot;\.phps$&quot;&gt;
        SetHandler application/x-httpd-php-source
    &lt;/FilesMatch&gt;
&lt;/IfModule&gt;
</pre>
<p>The <strong>index.php</strong> file is the standard test file to test the PHP distributions and the <strong>001_mod-php.conf</strong> is needed for FreeBSD only. As you can see the playbooks differ quite a bit for these three OSes. Once you deploy the playbooks, you can test the result by going to <strong>http://[nodeX]/index.php</strong>.<br />
Looks like this.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2021/01/P153-02.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2021/01/P153-02.png" alt="" width="983" height="227" class="aligncenter size-full wp-image-8802" srcset="https://blog.andreev.it/wp-content/uploads/2021/01/P153-02.png 983w, https://blog.andreev.it/wp-content/uploads/2021/01/P153-02-300x69.png 300w, https://blog.andreev.it/wp-content/uploads/2021/01/P153-02-768x177.png 768w, https://blog.andreev.it/wp-content/uploads/2021/01/P153-02-585x135.png 585w" sizes="(max-width: 983px) 100vw, 983px" /></a></p>
<h1>Service handlers</h1>
<p>Service handlers are used only when a change is made on the managed node. For example, we can restart a service only if the config file was changed. If the file is not changed, then there is no need to restart. Here is an example of a service handler. We&#8217;ll restart postfix service only if the main.cf file was changed.</p>
<pre class="brush: yaml; highlight: [11,14]; title: ; notranslate">
# service-handler.yml
---
- hosts: centos
  become: yes
  tasks:
  - name: Configure main.cf
    lineinfile:
      path: /etc/postfix/main.cf
      regexp: ^#mydomain
      line: 'mydomain = example.com'
    notify: restart postfix

  handlers:
  - name: restart postfix
    service: name=postfix state=restarted
</pre>
<p>Make sure that the name for the handler is the same (lines 11 and 14), so Ansible knows what service handlers is referred. </p>
<h1>Variables</h1>
<p>When ansible runs a playbook on a managed node, the first task is to gather info about the managed node. The info is a bunch of settings that we can use in our playbooks. For example, if you execute the following command, you can see the IP address, the CPU model, python version etc.</p>
<pre class="brush: bash; title: ; notranslate">
ansible -m setup &lt;node&gt;
</pre>
<p>We can use these settings and use them as variables if we need them. For example, this playbook displays the hostname and the IP.</p>
<pre class="brush: yaml; title: ; notranslate">
# showip.yml
---
  - hosts: freebsd
    become: yes
    tasks:
      - name: Show the IP address
        debug:
          msg: &quot;The hostname is {{ inventory_hostname}}  and the IP is {{ansible_default_ipv4.address }}&quot;
</pre>
<p>And if you run the playbook, you&#8217;ll see something like this.</p>
<pre class="brush: plain; title: ; notranslate">
TASK &#x5B;Show the IP address] *********************************************************************************************
ok: &#x5B;node1.andreev.local] =&gt; {
    &quot;msg&quot;: &quot;The hostname is node1.andreev.local  and the IP is 192.168.1.211&quot;
}
</pre>
<p>Here is another example of using variables. In this case, we&#8217;ll specify a file and change the ownership and the mode.</p>
<pre class="brush: yaml; title: ; notranslate">
# owner.yml
---
  - hosts: centos
    become: yes
    vars:
      filename: &quot;/var/www/html/index.php&quot;
    tasks:
      - name: Change the owner of the file
        file:
          path: &quot;{{ filename }}&quot;
          owner: apache
          group: apache
          mode: '0755'
</pre>
<p>In case we want to assign an output to a variable, we&#8217;ll have to use the keyword <strong>register</strong>. Here is an example of how to get the output from a command and print it on the screen with the keyword <strong>debug</strong>.</p>
<pre class="brush: yaml; title: ; notranslate">
# variables.yml
---
  - hosts: freebsd

    tasks:
    - name: Get the uptime manually
      command: uptime
      register: var_uptime

    - name: Print the uptime
      debug:
        msg: The uptime is &quot;{{ var_uptime }}&quot;
</pre>
<h1>Roles</h1>
<p>Roles let you automatically load related vars_files, tasks, handlers, and other Ansible artifacts based on a known file structure. Once you group your content in roles, you can easily reuse them and share them with other users. The idea is to separate the tasks, handlers and vars in different files. Let&#8217;s see this playbook for example. It changes a line in main.cf file, restarts postfix and copies a file under the postfix main directory.</p>
<pre class="brush: yaml; title: ; notranslate">
# roles.yml
---
  - hosts: centos
    become: yes

    vars:
      filevd: &quot;/etc/postfix/virtual_domains&quot;
      cfgpostfix: &quot;/etc/postfix/main.cf&quot;


    tasks:
    - name: Configure main.cf
      lineinfile:
        path: &quot;{{ cfgpostfix }}&quot;
        regexp: ^#mydomain
        line: 'mydomain = example.com'
      notify: restart postfix
    - name: Copy virtual_domains
      copy:
        src: ../files/virtual_domains
        dest: &quot;{{ filevd }}&quot;
        mode: 0755

    handlers:
    - name: restart postfix
      service: name=postfix state=restarted
</pre>
<p>We can rewrite this file by separating the tasks, variables, files and handlers. Run this command.</p>
<pre class="brush: bash; title: ; notranslate">
ansible-galaxy role init postfix
</pre>
<p>If you look at the file/directory structure of the newly created directory postfix, it looks like this.</p>
<pre class="brush: plain; highlight: [1]; title: ; notranslate">
tree postfix
postfix
├── defaults
│   └── main.yml
├── files
├── handlers
│   └── main.yml
├── meta
│   └── main.yml
├── README.md
├── tasks
│   └── main.yml
├── templates
├── tests
│   ├── inventory
│   └── test.yml
└── vars
    └── main.yml
</pre>
<p>Create a file <strong>virtual_domains</strong> under the <strong>files </strong>directory.</p>
<pre class="brush: bash; title: ; notranslate">
echo &quot;mydomain.com&quot; &gt; postfix/files/virtual_domains
</pre>
<p>Move the config for vars, handlers and tasks in the separate <strong>main.yml</strong> files. For example, this is how my files look like.<br />
<strong>postfix/vars/main.yml</strong></p>
<pre class="brush: yaml; title: ; notranslate">
---
# vars file for postfix

  filevd: &quot;/etc/postfix/virtual_domains&quot;
  cfgpostfix: &quot;/etc/postfix/main.cf&quot;
</pre>
<p>As you can see the keyword <strong>vars:</strong> does not exists. Ansible knows that this file is for <strong>vars </strong>so there is no need to enter the <strong>vars </strong>keyboard.<br />
<strong>postfix/tasks/main.yml</strong></p>
<pre class="brush: yaml; title: ; notranslate">
---
# tasks file for postfix

  - name: Configure main.cf
    lineinfile:
      path: &quot;{{ cfgpostfix }}&quot;
      regexp: ^#mydomain
      line: 'mydomain = example.com'
    notify: restart postfix
  - name: Copy virtual_domains
    copy:
      src: ../files/virtual_domains
      dest: &quot;{{ filevd }}&quot;
      mode: 0755
</pre>
<p><strong>postfix/handlers/main.yml</strong></p>
<pre class="brush: yaml; title: ; notranslate">
---
# handlers file for postfix

  - name: restart postfix
    service: name=postfix state=restarted
</pre>
<p>Finally, create a file called something.yml that will be your main file. This file has to be outside the postfix directory structure.<br />
In my case it looks like this.<br />
<strong>something.yml</strong></p>
<pre class="brush: yaml; title: ; notranslate">
# something.yml
---
  - hosts: centos
    become: yes
    roles:
      - postfix
</pre>
<p>Now, if you execute this playbook, ansible will automatically execute the rest of the dependant playbooks as well.</p>
<pre class="brush: bash; title: ; notranslate">
ansible-playbook something.yml
</pre>
<h1>Error handling</h1>
<p>Sometimes we want certain changes to be ignored. Sometimes, we know the behavior of certain commands and we know that they might return non-zero code and we want that ignored. For example, consider this part of a playbook.</p>
<pre class="brush: yaml; title: ; notranslate">
- hosts: centos
  tasks: 
  - name: Type something that will fail
    command: thiscommanddoesntexist
    ignore_errors: yes

  - name: Run command remotely
    command: /usr/local/bin/somecommand
    register: cmd_result
    changed_when: cmd_result == 2
</pre>
<p>Ansible would report a task as changed as long as the command (or) script gives zero return code.<br />
In the first part, we know that the task will fail, but we decide to ignore it using the keyword <strong>ignore_errors</strong>. No matter what the command returns, <strong>ignore_errors: yes</strong> will never report to ansible that the command failed.<br />
In the second command we can ignore the error based on the output of the command. For example, if the output is 2, the the error will be ignored. If cmd_result is not equal to 2, the task will be marked as changed.<br />
So whenever this condition is true, the task will be marked as changed. </p>
<h1>Tags</h1>
<p>Tags are used when you have a playbook with several tasks and you need to run only specific parts of it instead of running the entire playbook. You use tags to execute or skip selected tasks. Let&#8217;s say we have this playbook that installs Docker on Centos and has multiple tasks. As you can notice in lines 12, 24, 37 and 45 we have a new line with a keyword <strong>tags</strong> that we use to tag certain tasks. The purpose of this is to include or exclude these tasks from the playbook.</p>
<pre class="brush: yaml; highlight: [12,24,37,45]; title: ; notranslate">
# centos-docker.yml
---
- name: Install docker
  hosts: centos
  become: true

  tasks:
    - name: Install yum utils
      yum:
        name: yum-utils
        state: latest
      tags: install

    - name: Install device-mapper-persistent-data
      yum:
        name: device-mapper-persistent-data
        state: latest
      tags: install

    - name: Install lvm2
      yum:
        name: lvm2
        state: latest
      tags: install

    - name: Add Docker repo
      get_url:
        url: https://download.docker.com/linux/centos/docker-ce.repo
        dest: /etc/yum.repos.d/docer-ce.repo
      become: yes

    - name: Install Docker
      package:
        name: docker-ce
        state: latest
      become: yes
      tags: install

    - name: Start Docker service
      service:
        name: docker
        state: started
        enabled: yes
      become: yes
      tags: start
</pre>
<p>Now, with the command below, we can execute the playbook and only the tasks tagged with <strong>install </strong>will be executed.</p>
<pre class="brush: bash; title: ; notranslate">
ansible-playbook centos-docker.yaml --tags install
</pre>
<p>We can also tell ansible to NOT run those tasks tagged with <strong>install</strong>.</p>
<pre class="brush: bash; title: ; notranslate">
ansible-playbook centos-docker.yaml --skip-tags install
</pre>
<p>You can add multiple tags per task, e.g.</p>
<pre class="brush: yaml; title: ; notranslate">
tags:
  - cleanup_app
  - cleanup_web
</pre>
<p>Ansible reserves two tag names for special behavior: <strong>always </strong>and <strong>never</strong>. If you assign the <strong>always </strong>tag to a task or play, Ansible will always run that task or play, unless you specifically skip it (<strong>&#8211;skip-tags always</strong>). If you assign the <strong>never </strong>tag to a task or play, Ansible will skip that task or play unless you specifically request it (<strong>&#8211;tags never</strong>).</p>
<h1>Ansible Vault</h1>
<p>Ansible Vault encrypts variables and files so you can protect sensitive content such as passwords or keys rather than leaving it visible as plaintext in playbooks or roles.<br />
First, you have to create a vaulted file where we&#8217;ll store the passwords. When you run this command it will ask you to create a password and then an empty file will show up.</p>
<pre class="brush: bash; title: ; notranslate">
ansible-vault create secrets.yml
</pre>
<p>Add some passwords there and save the file.</p>
<pre class="brush: bash; title: ; notranslate">
mysql_pwd: &quot;DifficultPassword&quot;
ht_pwd: &quot;PasswordXYZ&quot;
</pre>
<p>If you look at the file now, you&#8217;ll see that it&#8217;s encrypted and you can&#8217;t see the passwords anymore.<br />
If you want to edit the file do <strong>ansible-vault edit secrets.yaml</strong> and enter the vault password.<br />
Create a small playbook that displays the password.</p>
<pre class="brush: yaml; title: ; notranslate">
# centos-vault.yml
---
- hosts: centos
  vars_files:
    - secrets.yml

  tasks:
  - name: Show mysql pwd
    debug:
      msg: &quot;{{ mysql_pwd }}&quot;
</pre>
<p>If you run the playbook now, ansible will throw an error saying ERROR! Attempting to decrypt but no vault secrets found. You have to specify the parameter <strong>&#8211;ask-vault-pass</strong> and enter the vault password when prompted.</p>
<h1>Prompts</h1>
<p>In case you need to pause the playbook execution and ask the user for some input such as confirmation or password, use prompts.<br />
If you want the output to echo, use <strong>private: no</strong>, otherwise what you type won&#8217;t show up on the screen. Here is an example of a playbook that asks you to confirm if a file needs to be copied to the node. If you type <strong>yes</strong> and hit enter, the file will be copied, otherwise it won&#8217;t.</p>
<pre class="brush: yaml; title: ; notranslate">
# centos-prompt.yml
---
- hosts: centos
  become: yes

  vars_prompt:
    name: upload
    private: no
    prompt: &quot;Do you want to upload xyz.txt?&quot;

  tasks:
  - name: Upload xyz.txt
    copy:
      src: xyz.txt
      dest: /var/log
    when: upload == &quot;yes&quot;
</pre>
<h1>Useful options</h1>
<pre class="brush: bash; title: ; notranslate">
ansible-playbook &lt;name&gt; --syntax-check # checks the syntax of the playbook
ansible-playbook &lt;name&gt; --check # does a dry run, reports the will-be changes, but the playbook is not executed
ansible-playbook &lt;name&gt; --step # ask to confirm each-step
</pre>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2021/02/ansible-quick-start-guide-for-freebsd-centos-and-ubuntu/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Visual Studio and BitBucket</title>
		<link>https://blog.andreev.it/2020/01/visual-studio-and-bitbucket/</link>
					<comments>https://blog.andreev.it/2020/01/visual-studio-and-bitbucket/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Sat, 11 Jan 2020 12:43:11 +0000</pubDate>
				<category><![CDATA[DevOps]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[BitBucket]]></category>
		<category><![CDATA[Visual Studio]]></category>
		<guid isPermaLink="false">https://blog.andreev.it/?p=6405</guid>

					<description><![CDATA[In this post I&#8217;ll explain how to use Visual Studio and BitBucket. I host&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>In this post I&#8217;ll explain how to use Visual Studio and BitBucket. I host my own BitBucket server, but it should be the same.<br />
First, create a BitBucket repository and name it. The repo will have to be the same name as the project in Visual Studio.<br />
So, in my case, I&#8217;ll create a repo called <strong>test123</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-01.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-01.png" alt="" width="586" height="355" class="aligncenter size-full wp-image-6407" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P143-01.png 586w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-01-300x182.png 300w" sizes="(max-width: 586px) 100vw, 586px" /></a><br />
Then, open Visual Studio 2019 and choose <strong>Continue without code</strong> from the bottom right.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-02.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-02.png" alt="" width="956" height="616" class="aligncenter size-full wp-image-6408" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P143-02.png 956w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-02-300x193.png 300w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-02-768x495.png 768w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-02-585x377.png 585w" sizes="(max-width: 956px) 100vw, 956px" /></a><br />
If you don&#8217;t have the BitBucket plugin, go to <strong>Extensions | Manage Extensions</strong> from the menu and install <strong>Visual Studio Bitbucket Extension</strong>. Once installed, you&#8217;ll have to close and re-open Visual Studio.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-03.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-03.png" alt="" width="603" height="318" class="aligncenter size-full wp-image-6409" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P143-03.png 603w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-03-300x158.png 300w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-03-585x309.png 585w" sizes="(max-width: 603px) 100vw, 603px" /></a><br />
Go to <strong>View | Team Explorer</strong> from the menu and click <strong>Login </strong>for the BitBucket exptension.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-04.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-04.png" alt="" width="298" height="419" class="aligncenter size-full wp-image-6412" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P143-04.png 298w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-04-213x300.png 213w" sizes="(max-width: 298px) 100vw, 298px" /></a><br />
Depending on your BitBucket server (cloud or on-prem), enter the necessary parameters and click to <strong>Connect</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-05.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-05.png" alt="" width="408" height="320" class="aligncenter size-full wp-image-6413" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P143-05.png 408w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-05-300x235.png 300w" sizes="(max-width: 408px) 100vw, 408px" /></a><br />
Once logged in, click on <strong>Clone</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-06.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-06.png" alt="" width="308" height="148" class="aligncenter size-full wp-image-6414" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P143-06.png 308w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-06-300x144.png 300w" sizes="(max-width: 308px) 100vw, 308px" /></a><br />
Select the repo and click <strong>Clone</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-07.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-07.png" alt="" width="461" height="295" class="aligncenter size-full wp-image-6415" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P143-07.png 461w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-07-300x192.png 300w" sizes="(max-width: 461px) 100vw, 461px" /></a><br />
Create a new project (<strong>File | New | Project</strong>) and name it the same as the repo.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-08.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-08.png" alt="" width="834" height="324" class="aligncenter size-full wp-image-6416" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P143-08.png 834w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-08-300x117.png 300w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-08-768x298.png 768w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-08-585x227.png 585w" sizes="(max-width: 834px) 100vw, 834px" /></a><br />
Click <strong>Next</strong> and name the project the same again, then click <strong>Create</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-09.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-09.png" alt="" width="980" height="668" class="aligncenter size-full wp-image-6417" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P143-09.png 980w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-09-300x204.png 300w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-09-768x523.png 768w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-09-585x399.png 585w" sizes="(max-width: 980px) 100vw, 980px" /></a><br />
You can push the initial code now. First click <strong>File | Save All</strong>. Then in Team Explorer, click where it says <strong>Connect </strong>and switch it to <strong>Change</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-10.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-10.png" alt="" width="262" height="194" class="aligncenter size-full wp-image-6419" /></a><br />
Type something for the first commit and click on <strong>Commit All and Push</strong>.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-11.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-11.png" alt="" width="403" height="324" class="aligncenter size-full wp-image-6420" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P143-11.png 403w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-11-300x241.png 300w" sizes="(max-width: 403px) 100vw, 403px" /></a><br />
If you go back to BitBucket, you&#8217;ll see your code there.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P143-12.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P143-12.png" alt="" width="613" height="306" class="aligncenter size-full wp-image-6422" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P143-12.png 613w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-12-300x150.png 300w, https://blog.andreev.it/wp-content/uploads/2019/12/P143-12-585x292.png 585w" sizes="(max-width: 613px) 100vw, 613px" /></a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2020/01/visual-studio-and-bitbucket/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>AWS: Deploy nginx HA cluster with Packer, Terraform and Ansible in a new VPC environment</title>
		<link>https://blog.andreev.it/2019/12/aws-deploy-nginx-ha-cluster-with-packer-terraform-and-ansible/</link>
					<comments>https://blog.andreev.it/2019/12/aws-deploy-nginx-ha-cluster-with-packer-terraform-and-ansible/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Sun, 29 Dec 2019 23:36:48 +0000</pubDate>
				<category><![CDATA[AWS]]></category>
		<category><![CDATA[Cloud]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[Ansible]]></category>
		<category><![CDATA[Packer]]></category>
		<category><![CDATA[Terraform]]></category>
		<guid isPermaLink="false">https://blog.andreev.it/?p=6372</guid>

					<description><![CDATA[In this post, I&#8217;ll explain how to create a whole environment consisting of a&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>In this post, I&#8217;ll explain how to create a whole environment consisting of a new VPC, two public subnets, two private subnets, a bastion host and two application hosts that run nginx web server. These application hosts will be behind an application load balancer. The purpose of the bastion host in the public subnet is to be able to access the application hosts which are in the private subnet and have no public access directly. We&#8217;ll use the official CentOS 7 image from the marketplace, make some modifications with packer and bake our own image that has a local ansible. Once these servers are deployed, they&#8217;ll already have nginx installed and started and a fully functional template website from GitHub (courtesy of <a href="https://www.free-css.com/free-css-templates" rel="noopener noreferrer" target="_blank">free-css.com</a>). This is how it looks like in a diagram.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P142-05.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P142-05.jpg" alt="" width="962" height="879" class="aligncenter size-full wp-image-6403" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P142-05.jpg 962w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-05-300x274.jpg 300w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-05-768x702.jpg 768w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-05-585x535.jpg 585w" sizes="(max-width: 962px) 100vw, 962px" /></a><br />
Before you start, I suggest you create a directory where you are going to place all these files. Make sure to execute each command from that directory unless told otherwise.</p>
<h1>Packer and ansible</h1>
<p>Packer is super easy to install. Just get the executable and place it somewhere. Once ready to create an image, specify your AWS access key and secret key. The IAM associated with these keys should be able to create an AMI. This is my packer file.</p>
<pre class="brush: xml; title: ; notranslate">
{
  &quot;variables&quot;: {
    &quot;aws_access_key&quot;: &quot;&quot;,
    &quot;aws_secret_key&quot;: &quot;&quot;,
    &quot;ami_name&quot;: &quot;centos7-USWTA&quot;,
    &quot;region&quot;: &quot;us-east-1&quot;,
    &quot;source_ami&quot;: &quot;ami-02eac2c0129f6376b&quot;,
    &quot;instance_type&quot;: &quot;t2.micro&quot;
  },
  &quot;builders&quot;: &#x5B;{
    &quot;access_key&quot;: &quot;{{user `aws_access_key`}}&quot;,
    &quot;secret_key&quot;: &quot;{{user `aws_secret_key`}}&quot;,
    &quot;ami_name&quot;: &quot;{{user `ami_name`}}.{{timestamp}}&quot;,
    &quot;region&quot; : &quot;{{user `region`}}&quot;,
    &quot;source_ami&quot;: &quot;{{user `source_ami`}}&quot;,
    &quot;instance_type&quot;: &quot;{{user `instance_type`}}&quot;,
    &quot;ssh_username&quot;: &quot;centos&quot;,
    &quot;type&quot;: &quot;amazon-ebs&quot;
  }],
  &quot;provisioners&quot;: &#x5B;
    {
     &quot;type&quot;: &quot;shell&quot;,
      &quot;inline&quot;: &quot;sudo yum -y install epel-release&quot;
    },
    {
      &quot;type&quot;: &quot;shell&quot;,
      &quot;inline&quot;: &quot;sudo yum -y install ansible&quot;
    },
    {
      &quot;type&quot;: &quot;file&quot;,
      &quot;source&quot;: &quot;ansible.yml&quot;,
      &quot;destination&quot;: &quot;/home/centos/ansible.yml&quot;
    },
    {
      &quot;type&quot;: &quot;ansible-local&quot;,
      &quot;playbook_file&quot;: &quot;ansible.yml&quot;
    }
  ]
}
</pre>
<p>You can specify your access keys in the script, but you don&#8217;t want to. You can specify the name of the AMI image, the region and the instance type. Do not change the source AMI variable, that&#8217;s the id of the CentOS official image. In the provisioners section, we&#8217;ll install the epel repo so we can install ansible. In addition, packer will get a local ansible playbook and place it under /home/centos in the new image. So, you need the <strong>ansible.yml</strong> playbook in the same directory as <strong>packer.json</strong> file. This is the ansible playbook. You definitely won&#8217;t do it this way in production. In my case I rename the original HTML root and clone a github repo. You&#8217;ll probably want to clone the site in a separate directory and use a modified <strong>nginx.conf</strong> file that suits your needs.</p>
<pre class="brush: xml; title: ; notranslate">
---
- hosts: localhost
  become: true

  tasks:
    - name: install nginx
      yum:
        name: nginx
        state: present

    - name: start nginx
      service:
        name: nginx
        state: started
        enabled: yes

    - name: install git
      yum:
        name: git
        state: present

    - name: remove the original html
      shell: |
        mv /usr/share/nginx/html /usr/share/nginx/html.old

    - name: clone web template
      git:
        repo: https://github.com/klimenta/webtemplate
        dest: /usr/share/nginx/html
        version: master
</pre>
<p>This playbook installs nginx, git and clones a website template from my github account. So, with both files ready (<strong>ansible.yml</strong> and <strong>packer.json</strong>), you can create your own image. Replace your own access key and secret and run this command.</p>
<pre class="brush: bash; title: ; notranslate">
packer build -var 'aws_access_key=AKIA.....' -var 'aws_secret_key=Dp123Sm4.....' packer.json
</pre>
<p>It takes about 5-6 minutes to bake the AMI. If you go to your AWS account, you&#8217;ll see it there.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P142-01.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P142-01.png" alt="" width="1501" height="475" class="aligncenter size-full wp-image-6374" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P142-01.png 1501w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-01-300x95.png 300w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-01-1024x324.png 1024w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-01-768x243.png 768w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-01-1170x370.png 1170w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-01-585x185.png 585w" sizes="(max-width: 1501px) 100vw, 1501px" /></a><br />
With our image ready, we are ready to deploy the infrastructure environment using Terraform. But before we do that, we want to create the keys that we will use to log in to our servers. In a production environment you&#8217;ll choose to have separate keys for the bastion host and the app hosts, but for the sake of clarity, we&#8217;ll use one key to rule them all.<br />
Type this command and hit Enter twice to skip the passphrase.</p>
<pre class="brush: bash; title: ; notranslate">
ssh-keygen -t rsa -f $PWD/keyUSWTA  -b 2048
</pre>
<p>You&#8217;ll have two files, <strong>keyUSWTA </strong>and <strong>keyUSWTA.pub</strong>. keyUSWTA is your private key, don&#8217;t share it with anyone. The public key will be used to spin-up the instances from our image. If you use putty instead of ssh, you have to <a href="https://www.puttygen.com/convert-pem-to-ppk" rel="noopener noreferrer" target="_blank">convert </a>your private key in a ppk format.</p>
<h1>Terraform</h1>
<p>Terraform is also super easy to install. Just copy the file somewhere in your path and you are ready to go. For Terraform we&#8217;ll have 4 separate files to build the infra. The first file is <strong>variables.tf</strong>. This is where we define all the variables that are in use. The second file is <strong>terraform.tfvars</strong>. This file is where we assign values to our variables. The first file can go to GitHub or any other version control system, the second one can not, especially if you have passwords and keys there. But it&#8217;s up to you. The third file is <strong>outputs.tf</strong>. Terraform dumps the values of this file when it&#8217;s done provisioning. For example, you want to see what&#8217;s your external IP from the instance you just created instead of going to AWS console to find out. And the fourth file is <strong>main.tf</strong> where we define our infrastructure. So, here they are.<br />
<strong>variables.tf</strong></p>
<pre class="brush: bash; title: ; notranslate">
variable &quot;aws_region&quot; {}
variable &quot;aws_az1&quot; {}
variable &quot;aws_az2&quot; {}
variable &quot;aws_profile&quot; {}
variable &quot;vpc_cidr&quot; {}
variable &quot;sub_private1_cidr&quot; {}
variable &quot;sub_private2_cidr&quot; {}
variable &quot;sub_public1_cidr&quot; {}
variable &quot;sub_public2_cidr&quot; {}
variable &quot;bastion_ami_id&quot; {}
variable &quot;instance_type&quot; {}
variable &quot;ip_address&quot; {}
</pre>
<p><strong>terraform.tfvars</strong></p>
<pre class="brush: bash; title: ; notranslate">
aws_profile = &quot;default&quot;
aws_region  = &quot;us-east-1&quot;
aws_az1 = &quot;us-east-1a&quot;
aws_az2 = &quot;us-east-1b&quot;
vpc_cidr = &quot;192.168.200.0/23&quot;
sub_public1_cidr = &quot;192.168.200.0/25&quot;
sub_public2_cidr = &quot;192.168.200.128/25&quot;
sub_private1_cidr = &quot;192.168.201.0/25&quot;
sub_private2_cidr = &quot;192.168.201.128/25&quot;
bastion_ami_id = &quot;ami-02eac2c0129f6376b&quot;
instance_type = &quot;t2.micro&quot;
ip_address = &quot;1.2.3.4/32&quot;
</pre>
<p>This means I&#8217;ll use my default profile for AWS CLI, the region for deployment is us-east-1, then the CIDRs for the VPC and the public and private subnets. The bastion host is based on the AMI there, which is CentOS 7 and t2.micro. And that host will have access from 1.2.3.4 IP only. Change to suit your needs.<br />
<strong>outputs.tf</strong></p>
<pre class="brush: bash; title: ; notranslate">
output &quot;aws_lb&quot; {
  value = aws_lb.albUSWTA.dns_name
}
output &quot;aws_ip&quot; {
  value = aws_instance.ec2USWTABastion.public_ip
}
output &quot;aws_app1_ip&quot; {
  value = aws_instance.ec2USWTAApplication1.private_ip
}
output &quot;aws_app2_ip&quot; {
  value = aws_instance.ec2USWTAApplication2.private_ip
}
</pre>
<p>These are the values that will be dumped on your screen, once Terraform completes. You&#8217;ll see the URL of your load balancer which is how you&#8217;ll access your website. Then, the public IP of the bastion host and the private IPs of your app instances in the private subnet.<br />
And finally, this is the <strong>main.tf</strong> file that provisions the infrastructure. Click the <strong>(+)</strong> sign to expand.</p>
<pre class="brush: bash; collapse: true; light: false; title: ; toolbar: true; notranslate">
provider &quot;aws&quot; {
  region  = var.aws_region
  profile = var.aws_profile
}

# VPC
resource &quot;aws_vpc&quot; &quot;vpcUSWTA&quot; {
  cidr_block = var.vpc_cidr

  tags = {
    Name = &quot;vpcUSWTA&quot;
  }
}

# Public subnet 1
resource &quot;aws_subnet&quot; &quot;subPublic1&quot; {
  vpc_id = aws_vpc.vpcUSWTA.id
  cidr_block = var.sub_public1_cidr
  availability_zone = var.aws_az1

  tags = {
    Name = &quot;subPublic - USWTA - 1&quot;
  }
}

# Public subnet 2
resource &quot;aws_subnet&quot; &quot;subPublic2&quot; {
  vpc_id = aws_vpc.vpcUSWTA.id
  cidr_block = var.sub_public2_cidr
  availability_zone = var.aws_az2

  tags = {
    Name = &quot;subPublic - USWTA - 2&quot;
  }
}

# Private subnet 1
resource &quot;aws_subnet&quot; &quot;subPrivate1&quot; {
  vpc_id = aws_vpc.vpcUSWTA.id
  cidr_block = var.sub_private1_cidr
  availability_zone = var.aws_az1

  tags = {
    Name = &quot;subPrivate - USWTA - 1&quot;
  }
}

# Private subnet 2
resource &quot;aws_subnet&quot; &quot;subPrivate2&quot; {
  vpc_id = aws_vpc.vpcUSWTA.id
  cidr_block = var.sub_private2_cidr
  availability_zone = var.aws_az2

  tags = {
    Name = &quot;subPrivate - USWTA - 2&quot;
  }
}

# Internet gateway
resource &quot;aws_internet_gateway&quot; &quot;igwUSWTA&quot; {
  vpc_id = aws_vpc.vpcUSWTA.id

  tags = {
    Name = &quot;igwInternetGateway - USWTA&quot;
  }
}

# Key pair
resource &quot;aws_key_pair&quot; &quot;keyUSWTA&quot; {
  key_name   = &quot;Key for USWTA&quot;
  public_key = file(&quot;${path.module}/keyUSWTA.pub&quot;)
}

# Security group for bastion host
resource &quot;aws_security_group&quot; &quot;sgUSWTABastion&quot; {
  name = &quot;sgUSWTA - Bastion&quot;
  description = &quot;Allow access on port 22 from restricted IP&quot;
  vpc_id = aws_vpc.vpcUSWTA.id

  ingress {
    from_port = 22
    to_port = 22
    protocol = &quot;tcp&quot;
    cidr_blocks = &#x5B;var.ip_address]
  }

  egress {
    from_port       = 0
    to_port         = 0
    protocol        = &quot;-1&quot;
    cidr_blocks     = &#x5B;&quot;0.0.0.0/0&quot;]
  }

  tags = {
    Name = &quot;Allow access on port 22 from my IP&quot;
  }
}

# Security group for application load balancer
resource &quot;aws_security_group&quot; &quot;sgUSWTAALB&quot; {
  name = &quot;sgUSWTA - ALB&quot;
  description = &quot;Allow access on port 80 from everywhere&quot;
  vpc_id = aws_vpc.vpcUSWTA.id

  ingress {
    from_port = 80
    to_port = 80
    protocol = &quot;tcp&quot;
    cidr_blocks = &#x5B;&quot;0.0.0.0/0&quot;]
  }

  egress {
    from_port       = 0
    to_port         = 0
    protocol        = &quot;-1&quot;
    cidr_blocks     = &#x5B;&quot;0.0.0.0/0&quot;]
  }

  tags = {
    Name = &quot;Allow HTTP access from everywhere&quot;
  }
}

# Security group for application hosts
resource &quot;aws_security_group&quot; &quot;sgUSWTAApplication&quot; {
  name = &quot;sgUSWTA - Application&quot;
  description = &quot;Allow access on ports 22 and 80&quot;
  vpc_id = aws_vpc.vpcUSWTA.id

  ingress {
    from_port = 22
    to_port = 22
    protocol = &quot;tcp&quot;
    cidr_blocks = &#x5B;var.sub_public1_cidr]
  }

  ingress {
    from_port = 80
    to_port = 80
    protocol = &quot;tcp&quot;
    security_groups = &#x5B;aws_security_group.sgUSWTAALB.id]
  }

  egress {
    from_port       = 0
    to_port         = 0
    protocol        = &quot;-1&quot;
    cidr_blocks     = &#x5B;&quot;0.0.0.0/0&quot;]
  }

  tags = {
    Name = &quot;Allow access on ports 22 and 80&quot;
  }
}

# Get the latest image
data &quot;aws_ami&quot; &quot;image&quot; {
  most_recent = true
  owners = &#x5B;&quot;self&quot;]
  filter {
    name = &quot;name&quot;
    values = &#x5B;&quot;centos7-USWTA*&quot;]
  }
}

# ec2 instance - bastion host
resource &quot;aws_instance&quot; &quot;ec2USWTABastion&quot; {
  ami = var.bastion_ami_id
  instance_type = var.instance_type
  key_name = aws_key_pair.keyUSWTA.key_name
  vpc_security_group_ids = &#x5B;aws_security_group.sgUSWTABastion.id]
  subnet_id = aws_subnet.subPublic1.id
  associate_public_ip_address = true

  root_block_device {
    delete_on_termination = true
  }

  tags = {
    Name = &quot;ec2USWTA - Bastion&quot;
  }
}

# ec2 instance - app host 1
resource &quot;aws_instance&quot; &quot;ec2USWTAApplication1&quot; {
  ami = data.aws_ami.image.id
  instance_type = var.instance_type
  key_name = aws_key_pair.keyUSWTA.key_name
  vpc_security_group_ids = &#x5B;aws_security_group.sgUSWTAApplication.id]
  subnet_id = aws_subnet.subPrivate1.id
  associate_public_ip_address = false

  root_block_device {
    delete_on_termination = true
  }

  tags = {
    Name = &quot;ec2USWTA - Application - 1&quot;
  }
}

# ec2 instance - app host 2
resource &quot;aws_instance&quot; &quot;ec2USWTAApplication2&quot; {
  ami = data.aws_ami.image.id
  instance_type = var.instance_type
  key_name = aws_key_pair.keyUSWTA.key_name
  vpc_security_group_ids = &#x5B;aws_security_group.sgUSWTAApplication.id]
  subnet_id = aws_subnet.subPrivate2.id
  associate_public_ip_address = false

  root_block_device {
    delete_on_termination = true
  }

  tags = {
    Name = &quot;ec2USWTA - Application - 2&quot;
  }
}

# Elastic IP for the NAT gateway
resource &quot;aws_eip&quot; &quot;eipUSWTA&quot; {
  vpc = true

  tags = {
    Name = &quot;eipUSWTA&quot;
  }
}

# NAT gateway
resource &quot;aws_nat_gateway&quot; &quot;ngwUSWTA&quot; {
  allocation_id = aws_eip.eipUSWTA.id
  subnet_id     = aws_subnet.subPublic1.id

  tags = {
    Name = &quot;ngwUSWTA&quot;
  }
}

# Add route to Internet to main route table
resource &quot;aws_route&quot; &quot;rtMainRoute&quot; {
  route_table_id = aws_vpc.vpcUSWTA.main_route_table_id
  destination_cidr_block = &quot;0.0.0.0/0&quot;
  gateway_id = aws_nat_gateway.ngwUSWTA.id
}

# Create public route table
resource &quot;aws_route_table&quot; &quot;rtPublic&quot; {
  vpc_id = aws_vpc.vpcUSWTA.id

  tags = {
    Name = &quot;rtPublic - USWTA&quot;
  }
}

# Add route to Internet to public route table
resource &quot;aws_route&quot; &quot;rtPublicRoute&quot; {
  route_table_id = aws_route_table.rtPublic.id
  destination_cidr_block = &quot;0.0.0.0/0&quot;
  gateway_id = aws_internet_gateway.igwUSWTA.id
}

# Associate public route table with public subnet 1
resource &quot;aws_route_table_association&quot; &quot;rtPubAssoc1&quot; {
  subnet_id   = aws_subnet.subPublic1.id
  route_table_id = aws_route_table.rtPublic.id
}

# Associate public route table with public subnet 2
resource &quot;aws_route_table_association&quot; &quot;rtPubAssoc2&quot; {
  subnet_id   = aws_subnet.subPublic2.id
  route_table_id = aws_route_table.rtPublic.id
}

# Application Load Balancer
resource &quot;aws_lb&quot; &quot;albUSWTA&quot; {
  name               = &quot;albUSWTA&quot;
  internal           = false
  load_balancer_type = &quot;application&quot;
  subnets            = &#x5B;aws_subnet.subPublic1.id, aws_subnet.subPublic2.id]
  security_groups = &#x5B;aws_security_group.sgUSWTAALB.id]

  tags = {
    Name = &quot;Application Load Balancer for USWTA&quot;
  }
}

# Target group
resource &quot;aws_lb_target_group&quot; &quot;tgUSWTA&quot; {
  name = &quot;tgUSWTA&quot;
  port = &quot;80&quot;
  protocol = &quot;HTTP&quot;
  vpc_id = aws_vpc.vpcUSWTA.id

  health_check {
    healthy_threshold   = 5
    unhealthy_threshold = 2
    timeout             = 5
    path                = &quot;/index.html&quot;
    port = 80
    matcher = &quot;200&quot;
    interval            = 30
  }
}

# Listener
resource &quot;aws_lb_listener&quot; &quot;lisUSWTA&quot; {
  load_balancer_arn = aws_lb.albUSWTA.arn
  port = &quot;80&quot;
  protocol = &quot;HTTP&quot;

  default_action {
    type = &quot;forward&quot;
    target_group_arn = aws_lb_target_group.tgUSWTA.arn
  }
}

# Add instance 1 to target group
resource &quot;aws_lb_target_group_attachment&quot; &quot;tgaUSWTA1&quot; {
  target_group_arn = aws_lb_target_group.tgUSWTA.arn
  target_id        = aws_instance.ec2USWTAApplication1.id
  port             = &quot;80&quot;
}

# Add instance 2 to target group
resource &quot;aws_lb_target_group_attachment&quot; &quot;tgaUSWTA2&quot; {
  target_group_arn = aws_lb_target_group.tgUSWTA.arn
  target_id        = aws_instance.ec2USWTAApplication2.id
  port             = &quot;80&quot;
}
</pre>
<p>The comments tell you what is being done. Terraform has a great documentation so if you google any of the resources created you&#8217;ll see ample documentation. So, make changes to suit your needs and once you have everything ready initialize the terraform so it downloads the necessary plugins.</p>
<pre class="brush: bash; title: ; notranslate">
terraform init
</pre>
<p>Pay special attention to lines <strong>68-72</strong>, it expects the keys to be the same exact filename and <strong>156-164</strong> it looks for an image that matches that name. That&#8217;s the AMI that we create with packer. Then create the infra. You have to type <strong>yes </strong>to proceed.</p>
<pre class="brush: bash; title: ; notranslate">
terraform apply
</pre>
<p>Once completed (in my case it took 4 minutes and 30 seconds), you&#8217;ll see the variables from the <strong>outputs.tf</strong>. </p>
<pre class="brush: bash; title: ; notranslate">
Apply complete! Resources: 25 added, 0 changed, 0 destroyed.

Outputs:

aws_app1_ip = 192.168.201.91
aws_app2_ip = 192.168.201.187
aws_ip = 54.145.191.102
aws_lb = albUSWTA-1695850209.us-east-1.elb.amazonaws.com
</pre>
<p>Go to <strong>http://albUSWTA-1695850209.us-east-1.elb.amazonaws.com</strong> which is the output from the LB from above and voila, your website is up and running on two HA servers.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P142-02.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P142-02.png" alt="" width="1472" height="751" class="aligncenter size-full wp-image-6390" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P142-02.png 1472w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-02-300x153.png 300w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-02-1024x522.png 1024w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-02-768x392.png 768w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-02-1170x597.png 1170w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-02-585x298.png 585w" sizes="(max-width: 1472px) 100vw, 1472px" /></a><br />
Go to your AWS console and you&#8217;ll see all of your resources there. e.g. the instances and the targets behind the load balancers.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/12/P142-03.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P142-03.png" alt="" width="488" height="97" class="aligncenter size-full wp-image-6395" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P142-03.png 488w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-03-300x60.png 300w" sizes="(max-width: 488px) 100vw, 488px" /></a><a href="https://blog.andreev.it/wp-content/uploads/2019/12/P142-04.png"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/12/P142-04.png" alt="" width="992" height="183" class="aligncenter size-full wp-image-6396" srcset="https://blog.andreev.it/wp-content/uploads/2019/12/P142-04.png 992w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-04-300x55.png 300w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-04-768x142.png 768w, https://blog.andreev.it/wp-content/uploads/2019/12/P142-04-585x108.png 585w" sizes="(max-width: 992px) 100vw, 992px" /></a><br />
Once you are done playing, destroy the resources. </p>
<pre class="brush: bash; title: ; notranslate">
terraform destroy
</pre>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2019/12/aws-deploy-nginx-ha-cluster-with-packer-terraform-and-ansible/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>FreeBSD, CentOS: Install Jenkins behind a nginx reverse proxy using SSL certificates</title>
		<link>https://blog.andreev.it/2019/07/freebsd-centos-install-jenkins-behind-a-nginx-reverse-proxy-using-ssl-certificates/</link>
					<comments>https://blog.andreev.it/2019/07/freebsd-centos-install-jenkins-behind-a-nginx-reverse-proxy-using-ssl-certificates/#respond</comments>
		
		<dc:creator><![CDATA[Kliment Andreev]]></dc:creator>
		<pubDate>Sat, 20 Jul 2019 13:06:24 +0000</pubDate>
				<category><![CDATA[CentOS]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[FreeBSD]]></category>
		<category><![CDATA[centos]]></category>
		<category><![CDATA[freebsd]]></category>
		<category><![CDATA[install]]></category>
		<category><![CDATA[Jenkins]]></category>
		<guid isPermaLink="false">https://blog.andreev.it/?p=5482</guid>

					<description><![CDATA[In this post, I&#8217;ll explain how to install Jenkins on FreeBSD and CentOS. Jenkins&#8230;]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><p>In this post, I&#8217;ll explain how to install <strong>Jenkins </strong>on FreeBSD and CentOS. <strong>Jenkins </strong>runs on port 8080 (8180 in FreeBSD), so sometimes it&#8217;s not possible to access these ports because of corporate firewalls. We&#8217;ll put <strong>Jenkins </strong>behind <strong>nginx </strong>that will act as a reverse proxy. We&#8217;ll use port 80 (HTTP) and 443 (HTTPS) if you want to have SSL certificates. The installation is very simple.</p>
<h1>CentOS</h1>
<p>The <strong>Jenkins </strong>package is not part of the default CentOS repository, so we have to download that one first, install <strong>Jenkins</strong>, enable it to start on boot and then start it. You can skip the first line if you have <strong>wget </strong>installed. By default, it is not installed.</p>
<pre class="brush: bash; title: ; notranslate">
yum -y install wget
wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo
rpm --import https://jenkins-ci.org/redhat/jenkins-ci.org.key
yum -y install jenkins java
systemctl enable jenkins
systemctl start jenkins
</pre>
<p>If you have a firewall enabled, you have to open the port for 8080.</p>
<pre class="brush: plain; title: ; notranslate">
firewall-cmd --permanent --zone=public --add-port=8080/tcp
firewall-cmd --reload
</pre>
<p>Now, go to <strong>http://[ip]:8080</strong> and replace the <ip> with the IP of your server and you should see this greeting page.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/07/P134-01.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/07/P134-01.jpg" alt="" width="869" height="355" class="aligncenter size-full wp-image-8637" srcset="https://blog.andreev.it/wp-content/uploads/2019/07/P134-01.jpg 869w, https://blog.andreev.it/wp-content/uploads/2019/07/P134-01-300x123.jpg 300w, https://blog.andreev.it/wp-content/uploads/2019/07/P134-01-768x314.jpg 768w, https://blog.andreev.it/wp-content/uploads/2019/07/P134-01-585x239.jpg 585w" sizes="(max-width: 869px) 100vw, 869px" /></a><br />
Cat out that file, copy &#038; paste the value in the browser and click <strong>Continue</strong>.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
cat /var/lib/jenkins/secrets/initialAdminPassword
7f09cb85a4b843bdab66c768020a7c6e
</pre>
<p>And that&#8217;s it. Follow the prompts to install plugins, provide admin password etc&#8230;</p>
<h2>nginx as reverse proxy</h2>
<p>In order to access the server with out the need to specify the port 8080 and use a DNS name, we&#8217;ll have to use <strong>nginx </strong>that will listen on port 80 or 443 and redirect the traffic to 8080. Let&#8217;s install and configure it. In my case, I&#8217;ll access the server as <strong>jenkins.domain.com</strong>.</p>
<pre class="brush: bash; title: ; notranslate">
yum -y install epel-release
yum -y install nginx
systemctl enable nginx
</pre>
<p>Edit <strong>/etc/nginx/nginx.conf</strong> and delete everything after this line around line 36. The <strong>include </strong>line should stay and the last line <strong>&#8220;}&#8221;</strong> should stay as well. So, the last lines should look like this.</p>
<pre class="brush: bash; title: ; notranslate">
       include /etc/nginx/conf.d/*.conf;
}
</pre>
<p>Go to <strong>/etc/nginx/conf.d</strong> folder and create a new file called <strong>jenkins.conf</strong>.<br />
This is how my <strong>jenkins.conf</strong> file looks like. Change the domain in the highlighted line.</p>
<pre class="brush: bash; highlight: [3]; title: ; notranslate">
server {
    listen 80;
    server_name jenkins.domain.com;

    location / {
        proxy_set_header   X-Forwarded-For $remote_addr;
        proxy_set_header   Host $http_host;
        proxy_pass         &quot;http://127.0.0.1:8080&quot;;
    }
}
</pre>
<p>Don&#8217;t forget to open port 80 in your firewall.</p>
<pre class="brush: plain; title: ; notranslate">
firewall-cmd --permanent --zone=public --add-port=80/tcp
firewall-cmd --reload
</pre>
<p>If you have SELinux, you&#8217;ll have to allow HTTP traffic.</p>
<pre class="brush: bash; title: ; notranslate">
setsebool -P httpd_can_network_connect 1
</pre>
<p>And restart nginx for changes to take effect.</p>
<pre class="brush: bash; title: ; notranslate">
systemctl restart nginx
</pre>
<p>Now, you can access the server as <strong>jenkins.domain.com</strong>.<br />
For HTTPS traffic, the configuration is different. You&#8217;ll need certificates specified in lines 15 and 16 (under <strong>/etc/nginx</strong>) and the domain changed in 7,13 and 31. Here is the config (<strong>/etc/nginx/conf.d/jenkins.conf</strong>) in order to access jenkins over SSL. </p>
<pre class="brush: bash; highlight: [7,13,15,16,31]; title: ; notranslate">
upstream jenkins {
  server 127.0.0.1:8080 fail_timeout=0;
}

server {
  listen 80;
  server_name jenkins.domain.com;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl;
  server_name jenkins.domain.com;

  ssl_certificate public_cert.pem;
  ssl_certificate_key private_cert.key;
  client_max_body_size 4M;

  location / {
    proxy_set_header        Host $host:$server_port;
    proxy_set_header        X-Real-IP $remote_addr;
    proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header        X-Forwarded-Proto $scheme;
    proxy_redirect http:// https://;
    proxy_pass              http://jenkins;
    # Required for new HTTP-based CLI
    proxy_http_version 1.1;
    proxy_request_buffering off;
    proxy_buffering off; # Required for HTTP-based CLI to work over SSL
    # workaround for https://issues.jenkins-ci.org/browse/JENKINS-45651
    add_header 'X-SSH-Endpoint' 'jenkins.domain.com:50022' always;
  }
}
</pre>
<p>Make sure you open port 443 on the firewall as we did for port 80 and change SELinux with <strong>setsebool</strong>. </p>
<pre class="brush: bash; title: ; notranslate">
firewall-cmd --permanent --zone=public --add-port=443/tcp
firewall-cmd --reload
</pre>
<p>SELinux change.</p>
<pre class="brush: bash; title: ; notranslate">
setsebool -P httpd_can_network_connect 1
</pre>
<p>You should also restart <strong>nginx </strong>for the changes to take effect.</p>
<pre class="brush: bash; title: ; notranslate">
systemctl restart nginx
</pre>
<p>That&#8217;s it. You should be able to access <strong>Jenkins </strong>as <strong>jenkins.domain.com</strong> over https.</p>
<h1>FreeBSD</h1>
<p>The installation on FreeBSD is a little bit different. You can install <strong>Jenkins </strong>from the packages. It will install JDK as well.</p>
<pre class="brush: bash; title: ; notranslate">
pkg install jenkins
</pre>
<p>The OpenJDK installation depends on these &#8220;memory filesystems&#8221;. Mount them now.</p>
<pre class="brush: bash; title: ; notranslate">
mount -t fdescfs fdesc /dev/fd
mount -t procfs proc /proc
</pre>
<p>&#8230;and make sure they are mounted on boot.</p>
<pre class="brush: bash; title: ; notranslate">
cat &lt;&lt;EOF &gt;&gt; /etc/fstab
fdesc   /dev/fd         fdescfs         rw      0       0
proc    /proc           procfs          rw      0       0
EOF
</pre>
<p>We need to make sure <strong>jenkins </strong>starts on boot.</p>
<pre class="brush: bash; title: ; notranslate">
sysrc jenkins_enable=YES
</pre>
<p>Now, we can start <strong>jenkins</strong>.</p>
<pre class="brush: bash; title: ; notranslate">
service jenkins start
</pre>
<p>Go to <strong>http://[IP]:8180/jenkins</strong> and you should see the welcome screen. Replace the <strong>[IP]</strong> with the IP address of your server. You should see this screen.<br />
<a href="https://blog.andreev.it/wp-content/uploads/2019/07/P134-02.jpg"><img loading="lazy" decoding="async" src="https://blog.andreev.it/wp-content/uploads/2019/07/P134-02.jpg" alt="" width="861" height="355" class="aligncenter size-full wp-image-8638" srcset="https://blog.andreev.it/wp-content/uploads/2019/07/P134-02.jpg 861w, https://blog.andreev.it/wp-content/uploads/2019/07/P134-02-300x124.jpg 300w, https://blog.andreev.it/wp-content/uploads/2019/07/P134-02-768x317.jpg 768w, https://blog.andreev.it/wp-content/uploads/2019/07/P134-02-585x241.jpg 585w" sizes="(max-width: 861px) 100vw, 861px" /></a><br />
Cat out that file, copy &#038; paste the value in the browser and click <strong>Continue</strong>.</p>
<pre class="brush: bash; highlight: [1]; title: ; notranslate">
cat /usr/local/jenkins/secrets/initialAdminPassword
c33a584df234433d9a88e19d8e14c289
</pre>
<p>And that’s it. Follow the prompts to install plugins, provide admin password etc…The jenkins installation on FreeBSD is accessed with a suffix <strong>/jenkins</strong>. If you try to access it without the suffix, you&#8217;ll get this error.</p>
<pre class="brush: bash; title: ; notranslate">
HTTP ERROR 404
Problem accessing /. Reason:
    Not Found
</pre>
<p>It&#8217;s much easier to do the redirects with nginx without the suffix, so we&#8217;ll change it.<br />
Edit this file <strong>/usr/local/etc/rc.d/jenkins</strong> and around line 54 find this.</p>
<pre class="brush: bash; title: ; notranslate">
: ${jenkins_args=&quot;--webroot=${jenkins_home}/war --httpPort=8180 --prefix=/jenkins&quot;}
</pre>
<p>Change it so it looks like this.</p>
<pre class="brush: bash; title: ; notranslate">
: ${jenkins_args=&quot;--webroot=${jenkins_home}/war --httpPort=8180 --prefix=/&quot;}
</pre>
<p>Restart <strong>Jenkins </strong>and you&#8217;ll be able to access it as <strong>http://[IP]:8180</strong>.</p>
<pre class="brush: bash; title: ; notranslate">
service jenkins restart
</pre>
<h2>nginx as reverse proxy</h2>
<p>In order to access the server with out the need to specify the port 8180 and use a DNS name, we&#8217;ll have to use nginx that will listen on port 80 or 443 and redirect the traffic to 8180. Let&#8217;s install and configure <strong>nginx</strong>. In my case, I&#8217;ll access the server as <strong>jenkins.domain.com</strong>.</p>
<pre class="brush: bash; title: ; notranslate">
pkg install nginx
</pre>
<p>Run on boot.</p>
<pre class="brush: bash; title: ; notranslate">
sysrc nginx_enable=YES
</pre>
<p>Edit <strong>/usr/local/etc/nginx/nginx.conf</strong> and delete everything after this line around line 39 after <strong>#gzip on</strong> directive. Add the include line, so the last three lines look like this.</p>
<pre class="brush: bash; title: ; notranslate">
    #gzip  on;
    include /usr/local/etc/nginx/conf.d/*.conf;
}
</pre>
<p>Create a new <strong>conf.d</strong> folder <strong>mkdir /usr/local/etc/nginx/conf.d</strong> and create a new file called <strong>jenkins.conf</strong>.<br />
This is how my <strong>jenkins.conf</strong> file looks like. Change the domain in the highlighted line.</p>
<pre class="brush: bash; highlight: [3]; title: ; notranslate">
server {
    listen 80;
    server_name jenkins.domain.com;

    location / {
        proxy_set_header   X-Forwarded-For $remote_addr;
        proxy_set_header   Host $http_host;
        proxy_pass         &quot;http://127.0.0.1:8180&quot;;
    }
}
</pre>
<p>Restart <strong>Jenkins </strong>and you should be able to access the site now with http://jenkins.domain.com.<br />
For HTTPS traffic, the configuration is different. You&#8217;ll need certificates specified in lines 15 and 16 (under <strong>/usr/local/etc/nginx</strong>) and the domain changed in 7,13 and 31. Here is the config (<strong>/usr/local/etc/nginx/conf.d/jenkins.conf</strong>) in order to access jenkins over SSL. </p>
<pre class="brush: bash; highlight: [7,13,15,16,31]; title: ; notranslate">
upstream jenkins {
  server 127.0.0.1:8180 fail_timeout=0;
}

server {
  listen 80;
  server_name jenkins.domain.com;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl;
  server_name jenkins.domain.com;

  ssl_certificate public_cert.pem;
  ssl_certificate_key private_cert.key;
  client_max_body_size 4M;

  location / {
    proxy_set_header        Host $host:$server_port;
    proxy_set_header        X-Real-IP $remote_addr;
    proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header        X-Forwarded-Proto $scheme;
    proxy_redirect http:// https://;
    proxy_pass              http://jenkins;
    # Required for new HTTP-based CLI
    proxy_http_version 1.1;
    proxy_request_buffering off;
    proxy_buffering off; # Required for HTTP-based CLI to work over SSL
    # workaround for https://issues.jenkins-ci.org/browse/JENKINS-45651
    add_header 'X-SSH-Endpoint' 'jenkins.domain.com:50022' always;
  }
}
</pre>
<p>You should also restart <strong>nginx </strong>for the changes to take effect.</p>
<pre class="brush: bash; title: ; notranslate">
service nginx restart
</pre>
<p>That&#8217;s it. You should be able to access <strong>Jenkins </strong>as <strong>jenkins.domain.com</strong> over https.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.andreev.it/2019/07/freebsd-centos-install-jenkins-behind-a-nginx-reverse-proxy-using-ssl-certificates/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
